-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Sftp key handling #39935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Sftp key handling #39935
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
5db8749
Improved RSA key handling for the SFTP storage
jvillafanez f0b9cf0
Include migration for the SFTP keys
jvillafanez 87754e6
Add unit tests
jvillafanez 42712c0
Fix missing import
jvillafanez b6d5fbb
Fix CI, keys won't be modified in any way
jvillafanez baf7b98
Add changelog entry
jvillafanez 80bff00
Add comments and missing docs
jvillafanez File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
106 changes: 106 additions & 0 deletions
106
apps/files_external/appinfo/Migrations/Version20220329110116.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| <?php | ||
| namespace OCA\files_external\Migrations; | ||
|
|
||
| use OC\NeedsUpdateException; | ||
| use OCA\Files_External\Lib\Backend\SFTP; | ||
| use OCA\Files_External\Lib\Auth\PublicKey\RSA; | ||
| use OCA\Files_External\Lib\RSAStore; | ||
| use OCP\Migration\ISimpleMigration; | ||
| use OCP\Migration\IOutput; | ||
| use OCP\Files\External\Service\IGlobalStoragesService; | ||
| use OCP\Files\External\IStorageConfig; | ||
| use OCP\ILogger; | ||
| use OCP\IConfig; | ||
| use phpseclib3\Crypt\RSA as RSACrypt; | ||
| use phpseclib3\Crypt\RSA\PrivateKey; | ||
|
|
||
| class Version20220329110116 implements ISimpleMigration { | ||
| /** @var IGlobalStoragesService */ | ||
| private $storageService; | ||
| /** @var ILogger */ | ||
| private $logger; | ||
| /** @var IConfig */ | ||
| private $config; | ||
|
|
||
| public function __construct(IGlobalStoragesService $storageService, ILogger $logger, IConfig $config) { | ||
| $this->storageService = $storageService; | ||
| $this->logger = $logger; | ||
| $this->config = $config; | ||
| } | ||
| /** | ||
| * @param IOutput $out | ||
| */ | ||
| public function run(IOutput $out) { | ||
| if (!$this->config->getSystemValue('installed', false)) { | ||
| // Skip the migration for new installations -> nothing to migrate | ||
| return; | ||
| } | ||
|
|
||
| $this->loadFSApps(); | ||
| \OC_Util::setupFS(); // this should load additional backends and auth mechanisms | ||
| $storageConfigs = $this->storageService->getStorageForAllUsers(); | ||
| $pass = $this->config->getSystemValue('secret', ''); | ||
|
|
||
| $rsaStore = RSAStore::getGlobalInstance(); | ||
| foreach ($storageConfigs as $storageConfig) { | ||
| if ($storageConfig->getBackend() instanceof SFTP && $storageConfig->getAuthMechanism() instanceof RSA) { | ||
| $encPubKey = $storageConfig->getBackendOption('public_key'); | ||
| $encPrivKey = $storageConfig->getBackendOption('private_key'); | ||
|
|
||
| $pubKey = \base64_decode($encPubKey, true); | ||
| $privKey = \base64_decode($encPrivKey, true); | ||
|
|
||
| $configId = $storageConfig->getId(); | ||
| if ($pubKey === false || $privKey === false) { | ||
| $out->warning("Storage configuration with id = {$configId}: Cannot decode either public or private key, skipping"); | ||
| continue; | ||
| } | ||
|
|
||
| try { | ||
| $rsaKey = RSACrypt::load($privKey, $pass)->withHash('sha1'); | ||
| } catch (\phpseclib3\Exception\NoKeyLoadedException $e) { | ||
| $out->warning("Storage configuration with id = {$configId}: Cannot load private key, skipping"); | ||
| continue; | ||
| } | ||
|
|
||
| $targetUserId = ''; | ||
| if ($storageConfig->getType() === IStorageConfig::MOUNT_TYPE_PERSONAl) { | ||
| $applicableUsers = $storageConfig->getApplicableUsers(); | ||
| $targetUserId = $applicableUsers[0]; // it must have one user. | ||
| } | ||
|
|
||
| $token = $rsaStore->storeData($rsaKey, $targetUserId); | ||
| $storageConfig->setBackendOption('public_key', $pubKey); | ||
| $storageConfig->setBackendOption('private_key', $token); | ||
|
|
||
| $this->storageService->updateStorage($storageConfig); | ||
| $out->info("Storage configuration with id = {$configId}: keys migrated successfully"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Load the FS apps. This is required because the FS apps might not be loaded during the | ||
| * migration. | ||
| */ | ||
| private function loadFSApps() { | ||
| $enabledApps = \OC_App::getEnabledApps(); | ||
| foreach ($enabledApps as $enabledApp) { | ||
| if ($enabledApp !== 'files_external' && \OC_App::isType($enabledApp, ['filesystem'])) { | ||
| try { | ||
| \OC_App::loadApp($enabledApp); | ||
| } catch (NeedsUpdateException $ex) { | ||
| if (\OC_App::updateApp($enabledApp)) { | ||
| // update successful. | ||
| // We can load the app without checking if the should upgrade or not. | ||
| \OC_App::loadApp($enabledApp, false); | ||
| } else { | ||
| $this->logger->error("Error during files_external migration. $enabledApp couldn't be loaded nor updated.", ['app' => 'files_external']); | ||
| $this->logger->logException($ex, ['app' => 'files_external']); | ||
| $this->logger->error("Mount points using $enabledApp might not be migrated properly. You might need to re-enter the passwords for those mount points", ['app' => 'files_external']); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| <?php | ||
| /** | ||
| * @author Juan Pablo Villafáñez Ramos <jvillafanez@owncloud.com> | ||
| * | ||
| * @copyright Copyright (c) 2022, ownCloud GmbH | ||
| * @license AGPL-3.0 | ||
| * | ||
| * This code is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU Affero General Public License, version 3, | ||
| * as published by the Free Software Foundation. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| * GNU Affero General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU Affero General Public License, version 3, | ||
| * along with this program. If not, see <http://www.gnu.org/licenses/> | ||
| * | ||
| */ | ||
|
|
||
| namespace OCA\Files_External\Lib; | ||
|
|
||
| use OCP\Security\ICredentialsManager; | ||
| use OCP\IConfig; | ||
| use phpseclib3\Crypt\RSA; | ||
| use phpseclib3\Crypt\RSA\PrivateKey; | ||
|
|
||
| /** | ||
| * Store and retrieve phpseclib3 RSA private keys | ||
| */ | ||
| class RSAStore { | ||
| private static $rsaStore = null; | ||
|
|
||
| /** @var ICredentialsManager */ | ||
| private $credentialsManager; | ||
| /** @var IConfig */ | ||
| private $config; | ||
|
|
||
| /** | ||
| * Get the global instance of the RSAStore. If no one is set yet, a new | ||
| * one will be created using real server components. | ||
| * @return RSAStore | ||
| */ | ||
| public static function getGlobalInstance(): RSAStore { | ||
| if (self::$rsaStore === null) { | ||
| self::$rsaStore = new RSAStore( | ||
| \OC::$server->getCredentialsManager(), | ||
| \OC::$server->getConfig() | ||
| ); | ||
| } | ||
| return self::$rsaStore; | ||
| } | ||
|
|
||
| /** | ||
| * Set a new RSAStore instance as a global instance overwriting whatever | ||
| * instance was there. | ||
| * This shouldn't be needed outside of unit tests | ||
|
jvillafanez marked this conversation as resolved.
|
||
| * @param RSAStore|null The RSAStore to be set as global instance, or null | ||
| * to destroy the global instance (destroying the global instance will allow | ||
| * getting the default one again) | ||
| */ | ||
| public static function setGlobalInstance(?RSAStore $rsaStore) { | ||
| self::$rsaStore = $rsaStore; | ||
| } | ||
|
|
||
| /** | ||
| * @param ICredentialsManager $credentialsManager | ||
| * @param IConfig $config | ||
| */ | ||
| public function __construct(ICredentialsManager $credentialsManager, IConfig $config) { | ||
| $this->credentialsManager = $credentialsManager; | ||
| $this->config = $config; | ||
| } | ||
|
|
||
| /** | ||
| * Store the $rsaKey inside the $userId's space. A token will be returned | ||
| * in order to retrieve the stored key | ||
| * @param PrivateKey $rsaKey the private key to be stored | ||
| * @param string $userId the user under which the token will be stored | ||
| * @return string an opaque token to be used to retrieve the stored key later | ||
| */ | ||
| public function storeData(PrivateKey $rsaKey, string $userId): string { | ||
| $password = $this->config->getSystemValue('secret', ''); | ||
| $privatekey = $rsaKey->withPassword($password)->toString('PKCS1'); | ||
|
|
||
| $keyId = \uniqid('rsaid:', true); | ||
|
|
||
| $this->credentialsManager->store($userId, $keyId, $privatekey); | ||
|
|
||
| $keyData = [ | ||
| 'rsaId' => $keyId, | ||
| 'userId' => $userId, | ||
| ]; | ||
| return \base64_encode(\json_encode($keyData)); | ||
| } | ||
|
|
||
| /** | ||
| * Retrieve a previously stored private key using the token that was returned | ||
| * when the key was stored | ||
| * @param string $token the token returned previously by the "storeData" | ||
| * method when the key was stored. | ||
| * @return PrivateKey the stored private key | ||
| */ | ||
| public function retrieveData(string $token): PrivateKey { | ||
| $keyData = \json_decode(\base64_decode($token), true); | ||
| $privateKey = $this->credentialsManager->retrieve($keyData['userId'], $keyData['rsaId']); | ||
| $password = $this->config->getSystemValue('secret', ''); | ||
|
|
||
| return RSA::load($privateKey, $password)->withHash('sha1'); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.