Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions apps/files_external/appinfo/Migrations/Version20220329110116.php
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']);
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion apps/files_external/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<admin>admin-external-storage</admin>
</documentation>
<rememberlogin>false</rememberlogin>
<version>0.8.0</version>
<version>0.9.0</version>
Comment thread
mrow4a marked this conversation as resolved.
<types>
<filesystem/>
</types>
Expand Down
18 changes: 15 additions & 3 deletions apps/files_external/js/public_key.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ $(document).ready(function() {
OCA.External.Settings.mountConfig.whenSelectAuthMechanism(function($tr, authMechanism, scheme, onCompletion) {
if (scheme === 'publickey') {
var config = $tr.find('.configuration');
if ($(config).find('[name="public_key_generate"]').length === 0) {
if (config.find('[name="public_key_generate"]').length === 0) {
setupTableRow($tr, config);
onCompletion.then(function() {
// If there's no private key, build one
if (0 === $(config).find('[data-parameter="private_key"]').val().length) {
var $privateKeyElem = config.find('[data-parameter="private_key"]');
if ($privateKeyElem.length !== 0 && $privateKeyElem.val().length === 0) {
// the private_key element might be removed in some scenarios such as
// global mount showing in the personal mounts
generateKeys($tr);
}
});
Expand All @@ -33,7 +36,16 @@ $(document).ready(function() {
function generateKeys(tr) {
var config = $(tr).find('.configuration');

$.post(OC.filePath('files_external', 'ajax', 'public_key.php'), {}, function(result) {
var $table = $(tr).parentsUntil('#files_external', '#externalStorage');
var isAdmin = $table.data('admin');

var postData = {};
if (!isAdmin) {
postData = {
'userId': OC.currentUser
};
}
$.post(OC.filePath('files_external', 'ajax', 'public_key.php'), postData, function(result) {
if (result && result.status === 'success') {
$(config).find('[data-parameter="public_key"]').val(result.data.public_key).keyup();
$(config).find('[data-parameter="private_key"]').val(result.data.private_key);
Expand Down
10 changes: 5 additions & 5 deletions apps/files_external/lib/Controller/AjaxController.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public function __construct($appName, IRequest $request, RSA $rsaMechanism) {
$this->rsaMechanism = $rsaMechanism;
}

private function generateSshKeys() {
$key = $this->rsaMechanism->createKey();
private function generateSshKeys($userId) {
$key = $this->rsaMechanism->createKey($userId);
// Replace the placeholder label with a more meaningful one
$key['publickey'] = \str_replace('phpseclib-generated-key', \gethostname(), $key['publickey']);

Expand All @@ -49,11 +49,11 @@ private function generateSshKeys() {

/**
* Generates an SSH public/private key pair.
*
* @param string $userId
* @NoAdminRequired
*/
public function getSshKeys() {
$key = $this->generateSshKeys();
public function getSshKeys($userId = '') {
$key = $this->generateSshKeys($userId);
return new JSONResponse(
['data' => [
'private_key' => $key['privatekey'],
Expand Down
42 changes: 14 additions & 28 deletions apps/files_external/lib/Lib/Auth/PublicKey/RSA.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@

use OCP\Files\External\Auth\AuthMechanism;
use OCP\Files\External\DefinitionParameter;
use OCP\Files\External\IStorageConfig;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IUser;
use OCA\Files_External\Lib\RSAStore;
use phpseclib3\Crypt\RSA as RSACrypt;

/**
Expand All @@ -36,12 +34,7 @@
class RSA extends AuthMechanism {
public const CREATE_KEY_BITS = 1024;

/** @var IConfig */
private $config;

public function __construct(IL10N $l, IConfig $config) {
$this->config = $config;

public function __construct(IL10N $l) {
$this
->setIdentifier('publickey::rsa')
->setScheme(self::SCHEME_PUBLICKEY)
Expand All @@ -56,33 +49,26 @@ public function __construct(IL10N $l, IConfig $config) {
;
}

public function manipulateStorageConfig(IStorageConfig &$storage, IUser $user = null) {
$privateKey = $storage->getBackendOption('private_key');
$password = $this->config->getSystemValue('secret', '');

try {
$rsaKey = RSACrypt::load($privateKey, $password)->withHash('sha1');
} catch (\phpseclib3\Exception\NoKeyLoadedException $e) {
throw new \RuntimeException('unable to load private key');
}

$storage->setBackendOption('private_key', \base64_encode($privateKey));
$storage->setBackendOption('public_key_auth', $rsaKey);
}

/**
* Generate a keypair
*
* Generate a keypair.
* The public key will be returned without any modification.
* The private key will be stored using the RSAStore, and a token will
* be returned instead. The token can be used to retrieve the private key
* from the RSAStore later.
* @params string $userId the userId holding the keys, or empty string if the keys are global
* (for system-wide mount points, for example)
* @return array ['privatekey' => $privateKey, 'publickey' => $publicKey]
*/
public function createKey() {
public function createKey($userId = '') {
/** @var RSACrypt\PrivateKey $rsaKey */
$rsaKey = RSACrypt::createKey(self::CREATE_KEY_BITS)
->withHash('sha1')
->withMGFHash('sha1');
$password = $this->config->getSystemValue('secret', '');

$rsaStore = RSAStore::getGlobalInstance();
$token = $rsaStore->storeData($rsaKey, $userId);
return [
'privatekey' => $rsaKey->withPassword($password)->toString('PKCS1'),
'privatekey' => $token,
'publickey' => $rsaKey->getPublicKey()->toString('OpenSSH')
];
}
Expand Down
112 changes: 112 additions & 0 deletions apps/files_external/lib/Lib/RSAStore.php
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
Comment thread
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');
}
}
8 changes: 6 additions & 2 deletions apps/files_external/lib/Lib/Storage/SFTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use Icewind\Streams\IteratorDirectory;
use Icewind\Streams\RetryWrapper;
use phpseclib3\Net\SFTP\Stream;
use OCA\Files_External\Lib\RSAStore;

/**
* Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to
Expand Down Expand Up @@ -92,8 +93,11 @@ public function __construct($params) {
}
$this->user = $params['user'];

if (isset($params['public_key_auth'])) {
$this->auth = $params['public_key_auth'];
if (isset($params['private_key'])) {
Comment thread
jvillafanez marked this conversation as resolved.
// The $params['private_key'] contains the token to get the private key, not the key.
// The actual private key is fetched from the RSAStore using that token.
$rsaStore = RSAStore::getGlobalInstance();
$this->auth = $rsaStore->retrieveData($params['private_key']);
} elseif (isset($params['password'])) {
$this->auth = $params['password'];
} else {
Expand Down
Loading