2021-06-23 10:46:01 -04:00
< ? php
2024-05-28 10:42:42 -04:00
2021-06-23 10:46:01 -04:00
/**
2024-05-28 10:42:42 -04:00
* SPDX - FileCopyrightText : 2021 - 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2019 ownCloud GmbH
* SPDX - License - Identifier : AGPL - 3.0 - only
2021-06-23 10:46:01 -04:00
*/
namespace OCA\Encryption\Command ;
2022-11-30 09:11:27 -05:00
use OC\Files\Storage\Wrapper\Encryption ;
2021-06-23 10:46:01 -04:00
use OC\Files\View ;
2021-08-11 03:18:16 -04:00
use OC\ServerNotAvailableException ;
2021-06-29 14:44:07 -04:00
use OCA\Encryption\Util ;
2025-05-09 10:37:06 -04:00
use OCP\Encryption\Exceptions\InvalidHeaderException ;
2026-01-26 16:09:40 -05:00
use OCP\Files\ISetupManager ;
2021-06-29 19:20:33 -04:00
use OCP\HintException ;
2021-06-24 03:31:52 -04:00
use OCP\IConfig ;
2022-11-29 11:48:17 -05:00
use OCP\IUser ;
2021-06-23 10:46:01 -04:00
use OCP\IUserManager ;
2023-06-29 09:41:40 -04:00
use Psr\Log\LoggerInterface ;
2021-06-23 10:46:01 -04:00
use Symfony\Component\Console\Command\Command ;
use Symfony\Component\Console\Input\InputArgument ;
use Symfony\Component\Console\Input\InputInterface ;
2022-11-29 11:48:17 -05:00
use Symfony\Component\Console\Input\InputOption ;
2021-06-23 10:46:01 -04:00
use Symfony\Component\Console\Output\OutputInterface ;
class FixEncryptedVersion extends Command {
2023-08-03 07:06:40 -04:00
private bool $supportLegacy = false ;
2021-08-11 03:18:16 -04:00
2021-06-29 14:44:07 -04:00
public function __construct (
2025-10-01 09:00:20 -04:00
private readonly IConfig $config ,
private readonly LoggerInterface $logger ,
private readonly IUserManager $userManager ,
private readonly Util $util ,
private readonly View $view ,
2026-01-26 16:09:40 -05:00
private readonly ISetupManager $setupManager ,
2021-06-29 14:44:07 -04:00
) {
2021-06-23 10:46:01 -04:00
parent :: __construct ();
}
2021-06-24 04:34:55 -04:00
protected function configure () : void {
2021-06-23 10:46:01 -04:00
parent :: configure ();
$this
-> setName ( 'encryption:fix-encrypted-version' )
-> setDescription ( 'Fix the encrypted version if the encrypted file(s) are not downloadable.' )
-> addArgument (
'user' ,
2022-11-29 11:48:17 -05:00
InputArgument :: OPTIONAL ,
2021-06-23 10:46:01 -04:00
'The id of the user whose files need fixing'
) -> addOption (
'path' ,
'p' ,
2022-11-29 11:48:17 -05:00
InputOption :: VALUE_REQUIRED ,
2021-06-23 10:46:01 -04:00
'Limit files to fix with path, e.g., --path="/Music/Artist". If path indicates a directory, all the files inside directory will be fixed.'
2022-11-29 11:48:17 -05:00
) -> addOption (
'all' ,
null ,
InputOption :: VALUE_NONE ,
'Run the fix for all users on the system, mutually exclusive with specifying a user id.'
2021-06-23 10:46:01 -04:00
);
}
2021-06-24 04:34:55 -04:00
protected function execute ( InputInterface $input , OutputInterface $output ) : int {
2022-12-04 15:36:10 -05:00
$skipSignatureCheck = $this -> config -> getSystemValueBool ( 'encryption_skip_signature_check' , false );
2021-08-11 03:18:16 -04:00
$this -> supportLegacy = $this -> config -> getSystemValueBool ( 'encryption.legacy_format_support' , false );
2021-06-24 03:31:52 -04:00
if ( $skipSignatureCheck ) {
$output -> writeln ( " <error>Repairing is not possible when \" encryption_skip_signature_check \" is set. Please disable this flag in the configuration.</error> \n " );
2023-08-03 07:06:40 -04:00
return self :: FAILURE ;
2021-06-24 03:31:52 -04:00
}
2021-06-29 14:44:07 -04:00
if ( ! $this -> util -> isMasterKeyEnabled ()) {
$output -> writeln ( " <error>Repairing only works with master key encryption.</error> \n " );
2023-08-03 07:06:40 -04:00
return self :: FAILURE ;
2021-06-29 14:44:07 -04:00
}
2022-11-29 11:48:17 -05:00
$user = $input -> getArgument ( 'user' );
$all = $input -> getOption ( 'all' );
2021-10-26 10:42:19 -04:00
$pathOption = \trim (( $input -> getOption ( 'path' ) ? ? '' ), '/' );
2021-06-23 10:46:01 -04:00
2023-08-03 07:20:33 -04:00
if ( ! $user && ! $all ) {
2024-08-23 09:10:27 -04:00
$output -> writeln ( 'Either a user id or --all needs to be provided' );
2023-08-03 07:20:33 -04:00
return self :: FAILURE ;
}
2022-11-29 11:48:17 -05:00
if ( $user ) {
if ( $all ) {
2024-08-23 09:10:27 -04:00
$output -> writeln ( 'Specifying a user id and --all are mutually exclusive' );
2023-08-03 07:06:40 -04:00
return self :: FAILURE ;
2022-11-29 11:48:17 -05:00
}
2025-10-01 09:00:20 -04:00
$user = $this -> userManager -> get ( $user );
if ( $user === null ) {
2022-11-29 11:48:17 -05:00
$output -> writeln ( " <error>User id $user does not exist. Please provide a valid user id</error> " );
2023-08-03 07:06:40 -04:00
return self :: FAILURE ;
2022-11-29 11:48:17 -05:00
}
2025-10-01 09:00:20 -04:00
return $this -> runForUser ( $user , $pathOption , $output ) ? self :: SUCCESS : self :: FAILURE ;
2021-06-23 10:46:01 -04:00
}
2023-08-03 07:20:33 -04:00
2025-10-01 09:00:20 -04:00
foreach ( $this -> userManager -> getSeenUsers () as $user ) {
2024-08-23 09:10:27 -04:00
$output -> writeln ( 'Processing files for ' . $user -> getUID ());
2025-10-01 09:00:20 -04:00
if ( ! $this -> runForUser ( $user , $pathOption , $output )) {
return self :: FAILURE ;
}
}
return self :: SUCCESS ;
2022-11-29 11:48:17 -05:00
}
2021-06-23 10:46:01 -04:00
2025-10-01 09:00:20 -04:00
private function runForUser ( IUser $user , string $pathOption , OutputInterface $output ) : bool {
$pathToWalk = '/' . $user -> getUID () . '/files' ;
2024-08-23 09:10:27 -04:00
if ( $pathOption !== '' ) {
2022-11-29 11:48:17 -05:00
$pathToWalk = " $pathToWalk / $pathOption " ;
2021-06-23 10:46:01 -04:00
}
return $this -> walkPathOfUser ( $user , $pathToWalk , $output );
}
2025-10-01 09:00:20 -04:00
private function walkPathOfUser ( IUser $user , string $path , OutputInterface $output ) : bool {
$this -> setupUserFileSystem ( $user );
2021-06-23 10:46:01 -04:00
if ( ! $this -> view -> file_exists ( $path )) {
2021-06-24 04:51:07 -04:00
$output -> writeln ( " <error>Path \" $path\ " does not exist . Please provide a valid path .</ error > " );
2025-10-01 09:00:20 -04:00
return false ;
2021-06-23 10:46:01 -04:00
}
if ( $this -> view -> is_file ( $path )) {
2021-06-24 04:51:07 -04:00
$output -> writeln ( " Verifying the content of file \" $path\ " " );
2021-06-23 10:46:01 -04:00
$this -> verifyFileContent ( $path , $output );
2025-10-01 09:00:20 -04:00
return true ;
2021-06-23 10:46:01 -04:00
}
$directories = [];
$directories [] = $path ;
while ( $root = \array_pop ( $directories )) {
$directoryContent = $this -> view -> getDirectoryContent ( $root );
foreach ( $directoryContent as $file ) {
$path = $root . '/' . $file [ 'name' ];
if ( $this -> view -> is_dir ( $path )) {
$directories [] = $path ;
} else {
2021-06-24 04:51:07 -04:00
$output -> writeln ( " Verifying the content of file \" $path\ " " );
2021-06-23 10:46:01 -04:00
$this -> verifyFileContent ( $path , $output );
}
}
}
2025-10-01 09:00:20 -04:00
return true ;
2021-06-23 10:46:01 -04:00
}
/**
* @ param bool $ignoreCorrectEncVersionCall , setting this variable to false avoids recursion
*/
2022-08-02 06:11:15 -04:00
private function verifyFileContent ( string $path , OutputInterface $output , bool $ignoreCorrectEncVersionCall = true ) : bool {
2021-06-23 10:46:01 -04:00
try {
2022-11-30 09:11:27 -05:00
// since we're manually poking around the encrypted state we need to ensure that this isn't cached in the encryption wrapper
$mount = $this -> view -> getMount ( $path );
$storage = $mount -> getStorage ();
if ( $storage && $storage -> instanceOfStorage ( Encryption :: class )) {
$storage -> clearIsEncryptedCache ();
}
2021-06-23 10:46:01 -04:00
/**
* In encryption , the files are read in a block size of 8192 bytes
* Read block size of 8192 and a bit more ( 808 bytes )
* If there is any problem , the first block should throw the signature
* mismatch error . Which as of now , is enough to proceed ahead to
* correct the encrypted version .
*/
$handle = $this -> view -> fopen ( $path , 'rb' );
2022-11-22 10:40:12 -05:00
if ( $handle === false ) {
$output -> writeln ( " <warning>Failed to open file: \" $path\ " skipping </ warning > " );
return true ;
}
2021-06-23 10:46:01 -04:00
if ( \fread ( $handle , 9001 ) !== false ) {
2022-08-02 06:47:26 -04:00
$fileInfo = $this -> view -> getFileInfo ( $path );
if ( ! $fileInfo ) {
$output -> writeln ( " <warning>File info not found for file: \" $path\ " </ warning > " );
return true ;
}
$encryptedVersion = $fileInfo -> getEncryptedVersion ();
$stat = $this -> view -> stat ( $path );
if (( $encryptedVersion == 0 ) && isset ( $stat [ 'hasHeader' ]) && ( $stat [ 'hasHeader' ] == true )) {
// The file has encrypted to false but has an encryption header
if ( $ignoreCorrectEncVersionCall === true ) {
// Lets rectify the file by correcting encrypted version
$output -> writeln ( " <info>Attempting to fix the path: \" $path\ " </ info > " );
return $this -> correctEncryptedVersion ( $path , $output );
}
return false ;
}
2021-06-24 04:51:07 -04:00
$output -> writeln ( " <info>The file \" $path\ " is : OK </ info > " );
2021-06-23 10:46:01 -04:00
}
\fclose ( $handle );
return true ;
2025-05-09 10:37:06 -04:00
} catch ( ServerNotAvailableException | InvalidHeaderException $e ) {
2021-08-11 03:18:16 -04:00
// not a "bad signature" error and likely "legacy cipher" exception
// this could mean that the file is maybe not encrypted but the encrypted version is set
if ( ! $this -> supportLegacy && $ignoreCorrectEncVersionCall === true ) {
$output -> writeln ( " <info>Attempting to fix the path: \" $path\ " </ info > " );
return $this -> correctEncryptedVersion ( $path , $output , true );
}
return false ;
2021-06-23 10:46:01 -04:00
} catch ( HintException $e ) {
2024-08-23 09:10:27 -04:00
$this -> logger -> warning ( 'Issue: ' . $e -> getMessage ());
2022-08-02 06:11:15 -04:00
// If allowOnce is set to false, this becomes recursive.
2021-06-23 10:46:01 -04:00
if ( $ignoreCorrectEncVersionCall === true ) {
2022-08-02 06:11:15 -04:00
// Lets rectify the file by correcting encrypted version
2021-06-24 04:51:07 -04:00
$output -> writeln ( " <info>Attempting to fix the path: \" $path\ " </ info > " );
2021-06-23 10:46:01 -04:00
return $this -> correctEncryptedVersion ( $path , $output );
}
return false ;
}
}
/**
2021-08-11 03:18:16 -04:00
* @ param bool $includeZero whether to try zero version for unencrypted file
2021-06-23 10:46:01 -04:00
*/
2022-08-02 06:11:15 -04:00
private function correctEncryptedVersion ( string $path , OutputInterface $output , bool $includeZero = false ) : bool {
2021-06-23 10:46:01 -04:00
$fileInfo = $this -> view -> getFileInfo ( $path );
2021-06-24 04:34:55 -04:00
if ( ! $fileInfo ) {
$output -> writeln ( " <warning>File info not found for file: \" $path\ " </ warning > " );
return true ;
}
2021-06-23 10:46:01 -04:00
$fileId = $fileInfo -> getId ();
2022-08-02 06:11:15 -04:00
if ( $fileId === null ) {
$output -> writeln ( " <warning>File info contains no id for file: \" $path\ " </ warning > " );
return true ;
}
2021-06-23 10:46:01 -04:00
$encryptedVersion = $fileInfo -> getEncryptedVersion ();
$wrongEncryptedVersion = $encryptedVersion ;
$storage = $fileInfo -> getStorage ();
$cache = $storage -> getCache ();
$fileCache = $cache -> get ( $fileId );
2021-06-24 04:34:55 -04:00
if ( ! $fileCache ) {
$output -> writeln ( " <warning>File cache entry not found for file: \" $path\ " </ warning > " );
return true ;
}
2021-06-23 10:46:01 -04:00
if ( $storage -> instanceOfStorage ( 'OCA\Files_Sharing\ISharedStorage' )) {
2021-06-24 04:34:55 -04:00
$output -> writeln ( " <info>The file: \" $path\ " is a share . Please also run the script for the owner of the share </ info > " );
2021-06-23 10:46:01 -04:00
return true ;
}
// Save original encrypted version so we can restore it if decryption fails with all version
$originalEncryptedVersion = $encryptedVersion ;
if ( $encryptedVersion >= 0 ) {
2021-08-11 03:18:16 -04:00
if ( $includeZero ) {
// try with zero first
$cacheInfo = [ 'encryptedVersion' => 0 , 'encrypted' => 0 ];
$cache -> put ( $fileCache -> getPath (), $cacheInfo );
2024-08-23 09:10:27 -04:00
$output -> writeln ( '<info>Set the encrypted version to 0 (unencrypted)</info>' );
2021-08-11 03:18:16 -04:00
if ( $this -> verifyFileContent ( $path , $output , false ) === true ) {
$output -> writeln ( " <info>Fixed the file: \" $path\ " with version 0 ( unencrypted ) </ info > " );
return true ;
}
}
2022-08-02 06:11:15 -04:00
// Test by decrementing the value till 1 and if nothing works try incrementing
2021-06-23 10:46:01 -04:00
$encryptedVersion -- ;
while ( $encryptedVersion > 0 ) {
$cacheInfo = [ 'encryptedVersion' => $encryptedVersion , 'encrypted' => $encryptedVersion ];
$cache -> put ( $fileCache -> getPath (), $cacheInfo );
$output -> writeln ( " <info>Decrement the encrypted version to $encryptedVersion </info> " );
if ( $this -> verifyFileContent ( $path , $output , false ) === true ) {
2024-08-23 09:10:27 -04:00
$output -> writeln ( " <info>Fixed the file: \" $path\ " with version " . $encryptedVersion . '</info>');
2021-06-23 10:46:01 -04:00
return true ;
}
$encryptedVersion -- ;
}
2022-08-02 06:11:15 -04:00
// So decrementing did not work. Now lets increment. Max increment is till 5
2021-06-23 10:46:01 -04:00
$increment = 1 ;
while ( $increment <= 5 ) {
/**
* The wrongEncryptedVersion would not be incremented so nothing to worry about here .
* Only the newEncryptedVersion is incremented .
* For example if the wrong encrypted version is 4 then
* cycle1 -> newEncryptedVersion = 5 ( 4 + 1 )
* cycle2 -> newEncryptedVersion = 6 ( 4 + 2 )
* cycle3 -> newEncryptedVersion = 7 ( 4 + 3 )
*/
$newEncryptedVersion = $wrongEncryptedVersion + $increment ;
$cacheInfo = [ 'encryptedVersion' => $newEncryptedVersion , 'encrypted' => $newEncryptedVersion ];
$cache -> put ( $fileCache -> getPath (), $cacheInfo );
$output -> writeln ( " <info>Increment the encrypted version to $newEncryptedVersion </info> " );
if ( $this -> verifyFileContent ( $path , $output , false ) === true ) {
2024-08-23 09:10:27 -04:00
$output -> writeln ( " <info>Fixed the file: \" $path\ " with version " . $newEncryptedVersion . '</info>');
2021-06-23 10:46:01 -04:00
return true ;
}
$increment ++ ;
}
}
$cacheInfo = [ 'encryptedVersion' => $originalEncryptedVersion , 'encrypted' => $originalEncryptedVersion ];
$cache -> put ( $fileCache -> getPath (), $cacheInfo );
2021-06-24 04:34:55 -04:00
$output -> writeln ( " <info>No fix found for \" $path\ " , restored version to original : $originalEncryptedVersion </ info > " );
2021-06-23 10:46:01 -04:00
return false ;
}
/**
* Setup user file system
*/
2025-10-01 09:00:20 -04:00
private function setupUserFileSystem ( IUser $user ) : void {
$this -> setupManager -> tearDown ();
$this -> setupManager -> setupForUser ( $user );
2021-06-23 10:46:01 -04:00
}
}