Skip to content

Commit 28a2104

Browse files
authored
Merge pull request #27726 from nextcloud/backport/27638/stable22
[stable22] Downstream encryption:fix-encrypted-version for repairing "bad signature" errors
2 parents e74f5ae + 3ca664d commit 28a2104

5 files changed

Lines changed: 635 additions & 0 deletions

File tree

apps/encryption/appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<command>OCA\Encryption\Command\DisableMasterKey</command>
4646
<command>OCA\Encryption\Command\RecoverUser</command>
4747
<command>OCA\Encryption\Command\ScanLegacyFormat</command>
48+
<command>OCA\Encryption\Command\FixEncryptedVersion</command>
4849
</commands>
4950

5051
<settings>

apps/encryption/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
'OCA\\Encryption\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
1111
'OCA\\Encryption\\Command\\DisableMasterKey' => $baseDir . '/../lib/Command/DisableMasterKey.php',
1212
'OCA\\Encryption\\Command\\EnableMasterKey' => $baseDir . '/../lib/Command/EnableMasterKey.php',
13+
'OCA\\Encryption\\Command\\FixEncryptedVersion' => $baseDir . '/../lib/Command/FixEncryptedVersion.php',
1314
'OCA\\Encryption\\Command\\RecoverUser' => $baseDir . '/../lib/Command/RecoverUser.php',
1415
'OCA\\Encryption\\Command\\ScanLegacyFormat' => $baseDir . '/../lib/Command/ScanLegacyFormat.php',
1516
'OCA\\Encryption\\Controller\\RecoveryController' => $baseDir . '/../lib/Controller/RecoveryController.php',

apps/encryption/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class ComposerStaticInitEncryption
2525
'OCA\\Encryption\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
2626
'OCA\\Encryption\\Command\\DisableMasterKey' => __DIR__ . '/..' . '/../lib/Command/DisableMasterKey.php',
2727
'OCA\\Encryption\\Command\\EnableMasterKey' => __DIR__ . '/..' . '/../lib/Command/EnableMasterKey.php',
28+
'OCA\\Encryption\\Command\\FixEncryptedVersion' => __DIR__ . '/..' . '/../lib/Command/FixEncryptedVersion.php',
2829
'OCA\\Encryption\\Command\\RecoverUser' => __DIR__ . '/..' . '/../lib/Command/RecoverUser.php',
2930
'OCA\\Encryption\\Command\\ScanLegacyFormat' => __DIR__ . '/..' . '/../lib/Command/ScanLegacyFormat.php',
3031
'OCA\\Encryption\\Controller\\RecoveryController' => __DIR__ . '/..' . '/../lib/Controller/RecoveryController.php',
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
<?php
2+
/**
3+
* @author Sujith Haridasan <sharidasan@owncloud.com>
4+
* @author Ilja Neumann <ineumann@owncloud.com>
5+
*
6+
* @copyright Copyright (c) 2019, ownCloud GmbH
7+
* @license AGPL-3.0
8+
*
9+
* This code is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License, version 3,
11+
* as published by the Free Software Foundation.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License, version 3,
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>
20+
*
21+
*/
22+
23+
namespace OCA\Encryption\Command;
24+
25+
use OC\Files\View;
26+
use OC\HintException;
27+
use OCA\Encryption\Util;
28+
use OCP\Files\IRootFolder;
29+
use OCP\IConfig;
30+
use OCP\ILogger;
31+
use OCP\IUserManager;
32+
use Symfony\Component\Console\Command\Command;
33+
use Symfony\Component\Console\Input\InputArgument;
34+
use Symfony\Component\Console\Input\InputInterface;
35+
use Symfony\Component\Console\Output\OutputInterface;
36+
37+
class FixEncryptedVersion extends Command {
38+
/** @var IConfig */
39+
private $config;
40+
41+
/** @var ILogger */
42+
private $logger;
43+
44+
/** @var IRootFolder */
45+
private $rootFolder;
46+
47+
/** @var IUserManager */
48+
private $userManager;
49+
50+
/** @var Util */
51+
private $util;
52+
53+
/** @var View */
54+
private $view;
55+
56+
public function __construct(
57+
IConfig $config,
58+
ILogger $logger,
59+
IRootFolder $rootFolder,
60+
IUserManager $userManager,
61+
Util $util,
62+
View $view
63+
) {
64+
$this->config = $config;
65+
$this->logger = $logger;
66+
$this->rootFolder = $rootFolder;
67+
$this->userManager = $userManager;
68+
$this->util = $util;
69+
$this->view = $view;
70+
parent::__construct();
71+
}
72+
73+
protected function configure(): void {
74+
parent::configure();
75+
76+
$this
77+
->setName('encryption:fix-encrypted-version')
78+
->setDescription('Fix the encrypted version if the encrypted file(s) are not downloadable.')
79+
->addArgument(
80+
'user',
81+
InputArgument::REQUIRED,
82+
'The id of the user whose files need fixing'
83+
)->addOption(
84+
'path',
85+
'p',
86+
InputArgument::OPTIONAL,
87+
'Limit files to fix with path, e.g., --path="/Music/Artist". If path indicates a directory, all the files inside directory will be fixed.'
88+
);
89+
}
90+
91+
/**
92+
* @param InputInterface $input
93+
* @param OutputInterface $output
94+
* @return int
95+
*/
96+
protected function execute(InputInterface $input, OutputInterface $output): int {
97+
$skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false);
98+
99+
if ($skipSignatureCheck) {
100+
$output->writeln("<error>Repairing is not possible when \"encryption_skip_signature_check\" is set. Please disable this flag in the configuration.</error>\n");
101+
return 1;
102+
}
103+
104+
if (!$this->util->isMasterKeyEnabled()) {
105+
$output->writeln("<error>Repairing only works with master key encryption.</error>\n");
106+
return 1;
107+
}
108+
109+
$user = (string)$input->getArgument('user');
110+
$pathToWalk = "/$user/files";
111+
112+
/**
113+
* trim() returns an empty string when the argument is an unset/null
114+
*/
115+
$pathOption = \trim($input->getOption('path'), '/');
116+
if ($pathOption !== "") {
117+
$pathToWalk = "$pathToWalk/$pathOption";
118+
}
119+
120+
if ($user === null) {
121+
$output->writeln("<error>No user id provided.</error>\n");
122+
return 1;
123+
}
124+
125+
if ($this->userManager->get($user) === null) {
126+
$output->writeln("<error>User id $user does not exist. Please provide a valid user id</error>");
127+
return 1;
128+
}
129+
return $this->walkPathOfUser($user, $pathToWalk, $output);
130+
}
131+
132+
/**
133+
* @param string $user
134+
* @param string $path
135+
* @param OutputInterface $output
136+
* @return int 0 for success, 1 for error
137+
*/
138+
private function walkPathOfUser($user, $path, OutputInterface $output): int {
139+
$this->setupUserFs($user);
140+
if (!$this->view->file_exists($path)) {
141+
$output->writeln("<error>Path \"$path\" does not exist. Please provide a valid path.</error>");
142+
return 1;
143+
}
144+
145+
if ($this->view->is_file($path)) {
146+
$output->writeln("Verifying the content of file \"$path\"");
147+
$this->verifyFileContent($path, $output);
148+
return 0;
149+
}
150+
$directories = [];
151+
$directories[] = $path;
152+
while ($root = \array_pop($directories)) {
153+
$directoryContent = $this->view->getDirectoryContent($root);
154+
foreach ($directoryContent as $file) {
155+
$path = $root . '/' . $file['name'];
156+
if ($this->view->is_dir($path)) {
157+
$directories[] = $path;
158+
} else {
159+
$output->writeln("Verifying the content of file \"$path\"");
160+
$this->verifyFileContent($path, $output);
161+
}
162+
}
163+
}
164+
return 0;
165+
}
166+
167+
/**
168+
* @param string $path
169+
* @param OutputInterface $output
170+
* @param bool $ignoreCorrectEncVersionCall, setting this variable to false avoids recursion
171+
*/
172+
private function verifyFileContent($path, OutputInterface $output, $ignoreCorrectEncVersionCall = true): bool {
173+
try {
174+
/**
175+
* In encryption, the files are read in a block size of 8192 bytes
176+
* Read block size of 8192 and a bit more (808 bytes)
177+
* If there is any problem, the first block should throw the signature
178+
* mismatch error. Which as of now, is enough to proceed ahead to
179+
* correct the encrypted version.
180+
*/
181+
$handle = $this->view->fopen($path, 'rb');
182+
183+
if (\fread($handle, 9001) !== false) {
184+
$output->writeln("<info>The file \"$path\" is: OK</info>");
185+
}
186+
187+
\fclose($handle);
188+
189+
return true;
190+
} catch (HintException $e) {
191+
$this->logger->warning("Issue: " . $e->getMessage());
192+
//If allowOnce is set to false, this becomes recursive.
193+
if ($ignoreCorrectEncVersionCall === true) {
194+
//Lets rectify the file by correcting encrypted version
195+
$output->writeln("<info>Attempting to fix the path: \"$path\"</info>");
196+
return $this->correctEncryptedVersion($path, $output);
197+
}
198+
return false;
199+
}
200+
}
201+
202+
/**
203+
* @param string $path
204+
* @param OutputInterface $output
205+
* @return bool
206+
*/
207+
private function correctEncryptedVersion($path, OutputInterface $output): bool {
208+
$fileInfo = $this->view->getFileInfo($path);
209+
if (!$fileInfo) {
210+
$output->writeln("<warning>File info not found for file: \"$path\"</warning>");
211+
return true;
212+
}
213+
$fileId = $fileInfo->getId();
214+
$encryptedVersion = $fileInfo->getEncryptedVersion();
215+
$wrongEncryptedVersion = $encryptedVersion;
216+
217+
$storage = $fileInfo->getStorage();
218+
219+
$cache = $storage->getCache();
220+
$fileCache = $cache->get($fileId);
221+
if (!$fileCache) {
222+
$output->writeln("<warning>File cache entry not found for file: \"$path\"</warning>");
223+
return true;
224+
}
225+
226+
if ($storage->instanceOfStorage('OCA\Files_Sharing\ISharedStorage')) {
227+
$output->writeln("<info>The file: \"$path\" is a share. Please also run the script for the owner of the share</info>");
228+
return true;
229+
}
230+
231+
// Save original encrypted version so we can restore it if decryption fails with all version
232+
$originalEncryptedVersion = $encryptedVersion;
233+
if ($encryptedVersion >= 0) {
234+
//test by decrementing the value till 1 and if nothing works try incrementing
235+
$encryptedVersion--;
236+
while ($encryptedVersion > 0) {
237+
$cacheInfo = ['encryptedVersion' => $encryptedVersion, 'encrypted' => $encryptedVersion];
238+
$cache->put($fileCache->getPath(), $cacheInfo);
239+
$output->writeln("<info>Decrement the encrypted version to $encryptedVersion</info>");
240+
if ($this->verifyFileContent($path, $output, false) === true) {
241+
$output->writeln("<info>Fixed the file: \"$path\" with version " . $encryptedVersion . "</info>");
242+
return true;
243+
}
244+
$encryptedVersion--;
245+
}
246+
247+
//So decrementing did not work. Now lets increment. Max increment is till 5
248+
$increment = 1;
249+
while ($increment <= 5) {
250+
/**
251+
* The wrongEncryptedVersion would not be incremented so nothing to worry about here.
252+
* Only the newEncryptedVersion is incremented.
253+
* For example if the wrong encrypted version is 4 then
254+
* cycle1 -> newEncryptedVersion = 5 ( 4 + 1)
255+
* cycle2 -> newEncryptedVersion = 6 ( 4 + 2)
256+
* cycle3 -> newEncryptedVersion = 7 ( 4 + 3)
257+
*/
258+
$newEncryptedVersion = $wrongEncryptedVersion + $increment;
259+
260+
$cacheInfo = ['encryptedVersion' => $newEncryptedVersion, 'encrypted' => $newEncryptedVersion];
261+
$cache->put($fileCache->getPath(), $cacheInfo);
262+
$output->writeln("<info>Increment the encrypted version to $newEncryptedVersion</info>");
263+
if ($this->verifyFileContent($path, $output, false) === true) {
264+
$output->writeln("<info>Fixed the file: \"$path\" with version " . $newEncryptedVersion . "</info>");
265+
return true;
266+
}
267+
$increment++;
268+
}
269+
}
270+
271+
$cacheInfo = ['encryptedVersion' => $originalEncryptedVersion, 'encrypted' => $originalEncryptedVersion];
272+
$cache->put($fileCache->getPath(), $cacheInfo);
273+
$output->writeln("<info>No fix found for \"$path\", restored version to original: $originalEncryptedVersion</info>");
274+
275+
return false;
276+
}
277+
278+
/**
279+
* Setup user file system
280+
* @param string $uid
281+
*/
282+
private function setupUserFs($uid): void {
283+
\OC_Util::tearDownFS();
284+
\OC_Util::setupFS($uid);
285+
}
286+
}

0 commit comments

Comments
 (0)