Browse Source
			
			
			Migrate SFTP_Key external storage to new API
			
				
		Migrate SFTP_Key external storage to new API
	
		
	
			
				The SFTP backend now supports public key authentication alongside password authentication.remotes/origin/db-empty-migrate
				 10 changed files with 177 additions and 287 deletions
			
			
		- 
					13apps/files_external/appinfo/app.php
- 
					4apps/files_external/appinfo/application.php
- 
					2apps/files_external/appinfo/routes.php
- 
					46apps/files_external/js/public_key.js
- 
					53apps/files_external/js/sftp_key.js
- 
					65apps/files_external/lib/auth/publickey/rsa.php
- 
					1apps/files_external/lib/backend/sftp.php
- 
					48apps/files_external/lib/backend/sftp_key.php
- 
					17apps/files_external/lib/sftp.php
- 
					215apps/files_external/lib/sftp_key.php
| @ -0,0 +1,46 @@ | |||
| $(document).ready(function() { | |||
| 
 | |||
| 	OCA.External.Settings.mountConfig.whenSelectAuthMechanism(function($tr, authMechanism, scheme) { | |||
| 		if (scheme === 'publickey') { | |||
| 			var config = $tr.find('.configuration'); | |||
| 			if ($(config).find('[name="public_key_generate"]').length === 0) { | |||
| 				setupTableRow($tr, config); | |||
| 			} | |||
| 		} | |||
| 	}); | |||
| 
 | |||
| 	$('#externalStorage').on('click', '[name="public_key_generate"]', function(event) { | |||
| 		event.preventDefault(); | |||
| 		var tr = $(this).parent().parent(); | |||
| 		generateKeys(tr); | |||
| 	}); | |||
| 
 | |||
| 	function setupTableRow(tr, config) { | |||
| 		$(config).append($(document.createElement('input')) | |||
| 			.addClass('button auth-param') | |||
| 			.attr('type', 'button') | |||
| 			.attr('value', t('files_external', 'Generate keys')) | |||
| 			.attr('name', 'public_key_generate') | |||
| 		); | |||
| 		// If there's no private key, build one
 | |||
| 		if (0 === $(config).find('[data-parameter="private_key"]').val().length) { | |||
| 			generateKeys(tr); | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	function generateKeys(tr) { | |||
| 		var config = $(tr).find('.configuration'); | |||
| 
 | |||
| 		$.post(OC.filePath('files_external', 'ajax', 'public_key.php'), {}, function(result) { | |||
| 			if (result && result.status === 'success') { | |||
| 				$(config).find('[data-parameter="public_key"]').val(result.data.public_key); | |||
| 				$(config).find('[data-parameter="private_key"]').val(result.data.private_key); | |||
| 				OCA.External.Settings.mountConfig.saveStorageConfig(tr, function() { | |||
| 					// Nothing to do
 | |||
| 				}); | |||
| 			} else { | |||
| 				OC.dialogs.alert(result.data.message, t('files_external', 'Error generating key pair') ); | |||
| 			} | |||
| 		}); | |||
| 	} | |||
| }); | |||
| @ -1,53 +0,0 @@ | |||
| $(document).ready(function() { | |||
| 
 | |||
| 	$('#externalStorage tbody tr.\\\\OC\\\\Files\\\\Storage\\\\SFTP_Key').each(function() { | |||
| 		var tr = $(this); | |||
| 		var config = $(tr).find('.configuration'); | |||
| 		if ($(config).find('.sftp_key').length === 0) { | |||
| 			setupTableRow(tr, config); | |||
| 		} | |||
| 	}); | |||
| 
 | |||
| 	// We can't catch the DOM elements being added, but we can pick up when
 | |||
| 	// they receive focus
 | |||
| 	$('#externalStorage').on('focus', 'tbody tr.\\\\OC\\\\Files\\\\Storage\\\\SFTP_Key', function() { | |||
| 		var tr = $(this); | |||
| 		var config = $(tr).find('.configuration'); | |||
| 
 | |||
| 		if ($(config).find('.sftp_key').length === 0) { | |||
| 			setupTableRow(tr, config); | |||
| 		} | |||
| 	}); | |||
| 
 | |||
| 	$('#externalStorage').on('click', '.sftp_key', function(event) { | |||
| 		event.preventDefault(); | |||
| 		var tr = $(this).parent().parent(); | |||
| 		generateKeys(tr); | |||
| 	}); | |||
| 
 | |||
| 	function setupTableRow(tr, config) { | |||
| 		$(config).append($(document.createElement('input')).addClass('button sftp_key') | |||
| 			.attr('type', 'button') | |||
| 			.attr('value', t('files_external', 'Generate keys'))); | |||
| 		// If there's no private key, build one
 | |||
| 		if (0 === $(config).find('[data-parameter="private_key"]').val().length) { | |||
| 			generateKeys(tr); | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	function generateKeys(tr) { | |||
| 		var config = $(tr).find('.configuration'); | |||
| 
 | |||
| 		$.post(OC.filePath('files_external', 'ajax', 'sftp_key.php'), {}, function(result) { | |||
| 			if (result && result.status === 'success') { | |||
| 				$(config).find('[data-parameter="public_key"]').val(result.data.public_key); | |||
| 				$(config).find('[data-parameter="private_key"]').val(result.data.private_key); | |||
| 				OCA.External.mountConfig.saveStorageConfig(tr, function() { | |||
| 					// Nothing to do
 | |||
| 				}); | |||
| 			} else { | |||
| 				OC.dialogs.alert(result.data.message, t('files_external', 'Error generating key pair') ); | |||
| 			} | |||
| 		}); | |||
| 	} | |||
| }); | |||
| @ -0,0 +1,65 @@ | |||
| <?php | |||
| /** | |||
|  * @author Robin McCorkell <rmccorkell@owncloud.com> | |||
|  * | |||
|  * @copyright Copyright (c) 2015, ownCloud, Inc. | |||
|  * @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\Auth\PublicKey; | |||
| 
 | |||
| use \OCP\IL10N; | |||
| use \OCA\Files_External\Lib\DefinitionParameter; | |||
| use \OCA\Files_External\Lib\Auth\AuthMechanism; | |||
| use \OCA\Files_External\Lib\StorageConfig; | |||
| use \OCP\IConfig; | |||
| use \phpseclib\Crypt\RSA as RSACrypt; | |||
| 
 | |||
| /** | |||
|  * RSA public key authentication | |||
|  */ | |||
| class RSA extends AuthMechanism { | |||
| 
 | |||
| 	/** @var IConfig */ | |||
| 	private $config; | |||
| 
 | |||
| 	public function __construct(IL10N $l, IConfig $config) { | |||
| 		$this->config = $config; | |||
| 
 | |||
| 		$this | |||
| 			->setIdentifier('publickey::rsa') | |||
| 			->setScheme(self::SCHEME_PUBLICKEY) | |||
| 			->setText($l->t('RSA public key')) | |||
| 			->addParameters([ | |||
| 				(new DefinitionParameter('user', $l->t('Username'))), | |||
| 				(new DefinitionParameter('public_key', $l->t('Public key'))), | |||
| 				(new DefinitionParameter('private_key', 'private_key')) | |||
| 					->setType(DefinitionParameter::VALUE_HIDDEN), | |||
| 			]) | |||
| 			->setCustomJs('public_key') | |||
| 		; | |||
| 	} | |||
| 
 | |||
| 	public function manipulateStorageConfig(StorageConfig &$storage) { | |||
| 		$auth = new RSACrypt(); | |||
| 		$auth->setPassword($this->config->getSystemValue('secret', '')); | |||
| 		if (!$auth->loadKey($storage->getBackendOption('private_key'))) { | |||
| 			throw new \RuntimeException('unable to load private key'); | |||
| 		} | |||
| 		$storage->setBackendOption('public_key_auth', $auth); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,48 @@ | |||
| <?php | |||
| /** | |||
|  * @author Robin McCorkell <rmccorkell@owncloud.com> | |||
|  * | |||
|  * @copyright Copyright (c) 2015, ownCloud, Inc. | |||
|  * @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\Backend; | |||
| 
 | |||
| use \OCP\IL10N; | |||
| use \OCA\Files_External\Lib\Backend\Backend; | |||
| use \OCA\Files_External\Lib\DefinitionParameter; | |||
| use \OCA\Files_External\Lib\Auth\AuthMechanism; | |||
| use \OCA\Files_External\Service\BackendService; | |||
| use \OCA\Files_External\Lib\Auth\PublicKey\RSA; | |||
| 
 | |||
| class SFTP_Key extends Backend { | |||
| 
 | |||
| 	public function __construct(IL10N $l, RSA $legacyAuth) { | |||
| 		$this | |||
| 			->setIdentifier('\OC\Files\Storage\SFTP_Key') | |||
| 			->setStorageClass('\OC\Files\Storage\SFTP') | |||
| 			->setText($l->t('SFTP with secret key login [DEPRECATED]')) | |||
| 			->addParameters([ | |||
| 				(new DefinitionParameter('host', $l->t('Host'))), | |||
| 				(new DefinitionParameter('root', $l->t('Remote subfolder'))) | |||
| 					->setFlag(DefinitionParameter::FLAG_OPTIONAL), | |||
| 			]) | |||
| 			->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY) | |||
| 			->setLegacyAuthMechanism($legacyAuth) | |||
| 		; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -1,215 +0,0 @@ | |||
| <?php | |||
| /** | |||
|  * @author Lukas Reschke <lukas@owncloud.com> | |||
|  * @author Morris Jobke <hey@morrisjobke.de> | |||
|  * @author Ross Nicoll <jrn@jrn.me.uk> | |||
|  * | |||
|  * @copyright Copyright (c) 2015, ownCloud, Inc. | |||
|  * @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 OC\Files\Storage; | |||
| 
 | |||
| use phpseclib\Crypt\RSA; | |||
| 
 | |||
| class SFTP_Key extends \OC\Files\Storage\SFTP { | |||
| 	private $publicKey; | |||
| 	private $privateKey; | |||
| 
 | |||
| 	/** | |||
| 	 * {@inheritdoc} | |||
| 	 */ | |||
| 	public function __construct($params) { | |||
| 		parent::__construct($params); | |||
| 		$this->publicKey = $params['public_key']; | |||
| 		$this->privateKey = $params['private_key']; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * Returns the connection. | |||
| 	 * | |||
| 	 * @return \phpseclib\Net\SFTP connected client instance | |||
| 	 * @throws \Exception when the connection failed | |||
| 	 */ | |||
| 	public function getConnection() { | |||
| 		if (!is_null($this->client)) { | |||
| 			return $this->client; | |||
| 		} | |||
| 
 | |||
| 		$hostKeys = $this->readHostKeys(); | |||
| 		$this->client = new \phpseclib\Net\SFTP($this->getHost()); | |||
| 
 | |||
| 		// The SSH Host Key MUST be verified before login().
 | |||
| 		$currentHostKey = $this->client->getServerPublicHostKey(); | |||
| 		if (array_key_exists($this->getHost(), $hostKeys)) { | |||
| 			if ($hostKeys[$this->getHost()] !== $currentHostKey) { | |||
| 				throw new \Exception('Host public key does not match known key'); | |||
| 			} | |||
| 		} else { | |||
| 			$hostKeys[$this->getHost()] = $currentHostKey; | |||
| 			$this->writeHostKeys($hostKeys); | |||
| 		} | |||
| 
 | |||
| 		$key = $this->getPrivateKey(); | |||
| 		if (is_null($key)) { | |||
| 			throw new \Exception('Secret key could not be loaded'); | |||
| 		} | |||
| 		if (!$this->client->login($this->getUser(), $key)) { | |||
| 			throw new \Exception('Login failed'); | |||
| 		} | |||
| 		return $this->client; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * Returns the private key to be used for authentication to the remote server. | |||
| 	 * | |||
| 	 * @return RSA instance or null in case of a failure to load the key. | |||
| 	 */ | |||
| 	private function getPrivateKey() { | |||
| 		$key = new RSA(); | |||
| 		$key->setPassword(\OC::$server->getConfig()->getSystemValue('secret', '')); | |||
| 		if (!$key->loadKey($this->privateKey)) { | |||
| 			// Should this exception rather than return null?
 | |||
| 			return null; | |||
| 		} | |||
| 		return $key; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * Throws an exception if the provided host name/address is invalid (cannot be resolved | |||
| 	 * and is not an IPv4 address). | |||
| 	 * | |||
| 	 * @return true; never returns in case of a problem, this return value is used just to | |||
| 	 * make unit tests happy. | |||
| 	 */ | |||
| 	public function assertHostAddressValid($hostname) { | |||
| 		// TODO: Should handle IPv6 addresses too
 | |||
| 		if (!preg_match('/^\d+\.\d+\.\d+\.\d+$/', $hostname) && gethostbyname($hostname) === $hostname) { | |||
| 			// Hostname is not an IPv4 address and cannot be resolved via DNS
 | |||
| 			throw new \InvalidArgumentException('Cannot resolve hostname.'); | |||
| 		} | |||
| 		return true; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * Throws an exception if the provided port number is invalid (cannot be resolved | |||
| 	 * and is not an IPv4 address). | |||
| 	 * | |||
| 	 * @return true; never returns in case of a problem, this return value is used just to | |||
| 	 * make unit tests happy. | |||
| 	 */ | |||
| 	public function assertPortNumberValid($port) { | |||
| 		if (!preg_match('/^\d+$/', $port)) { | |||
| 			throw new \InvalidArgumentException('Port number must be a number.'); | |||
| 		} | |||
| 		if ($port < 0 || $port > 65535) { | |||
| 			throw new \InvalidArgumentException('Port number must be between 0 and 65535 inclusive.'); | |||
| 		} | |||
| 		return true; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * Replaces anything that's not an alphanumeric character or "." in a hostname | |||
| 	 * with "_", to make it safe for use as part of a file name. | |||
| 	 */ | |||
| 	protected function sanitizeHostName($name) { | |||
| 		return preg_replace('/[^\d\w\._]/', '_', $name); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * Replaces anything that's not an alphanumeric character or "_" in a username | |||
| 	 * with "_", to make it safe for use as part of a file name. | |||
| 	 */ | |||
| 	protected function sanitizeUserName($name) { | |||
| 		return preg_replace('/[^\d\w_]/', '_', $name); | |||
| 	} | |||
| 
 | |||
| 	public function test() { | |||
| 
 | |||
| 		// FIXME: Use as expression in empty once PHP 5.4 support is dropped
 | |||
| 		$host = $this->getHost(); | |||
| 		if (empty($host)) { | |||
| 			\OC::$server->getLogger()->warning('Hostname has not been specified'); | |||
| 			return false; | |||
| 		} | |||
| 		// FIXME: Use as expression in empty once PHP 5.4 support is dropped
 | |||
| 		$user = $this->getUser(); | |||
| 		if (empty($user)) { | |||
| 			\OC::$server->getLogger()->warning('Username has not been specified'); | |||
| 			return false; | |||
| 		} | |||
| 		if (!isset($this->privateKey)) { | |||
| 			\OC::$server->getLogger()->warning('Private key was missing from the request'); | |||
| 			return false; | |||
| 		} | |||
| 
 | |||
| 		// Sanity check the host
 | |||
| 		$hostParts = explode(':', $this->getHost()); | |||
| 		try { | |||
| 			if (count($hostParts) == 1) { | |||
| 				$hostname = $hostParts[0]; | |||
| 				$this->assertHostAddressValid($hostname); | |||
| 			} else if (count($hostParts) == 2) { | |||
| 				$hostname = $hostParts[0]; | |||
| 				$this->assertHostAddressValid($hostname); | |||
| 				$this->assertPortNumberValid($hostParts[1]); | |||
| 			} else { | |||
| 				throw new \Exception('Host connection string is invalid.'); | |||
| 			} | |||
| 		} catch(\Exception $e) { | |||
| 			\OC::$server->getLogger()->warning($e->getMessage()); | |||
| 			return false; | |||
| 		} | |||
| 
 | |||
| 		// Validate the key
 | |||
| 		$key = $this->getPrivateKey(); | |||
| 		if (is_null($key)) { | |||
| 			\OC::$server->getLogger()->warning('Secret key could not be loaded'); | |||
| 			return false; | |||
| 		} | |||
| 
 | |||
| 		try { | |||
| 			if ($this->getConnection()->nlist() === false) { | |||
| 				return false; | |||
| 			} | |||
| 		} catch(\Exception $e) { | |||
| 			// We should be throwing a more specific error, so we're not just catching
 | |||
| 			// Exception here
 | |||
| 			\OC::$server->getLogger()->warning($e->getMessage()); | |||
| 			return false; | |||
| 		} | |||
| 
 | |||
| 		// Save the key somewhere it can easily be extracted later
 | |||
| 		if (\OC::$server->getUserSession()->getUser()) { | |||
| 			$view = new \OC\Files\View('/'.\OC::$server->getUserSession()->getUser()->getUId().'/files_external/sftp_keys'); | |||
| 			if (!$view->is_dir('')) { | |||
| 				if (!$view->mkdir('')) { | |||
| 					\OC::$server->getLogger()->warning('Could not create secret key directory.'); | |||
| 					return false; | |||
| 				} | |||
| 			} | |||
| 			$key_filename = $this->sanitizeUserName($this->getUser()).'@'.$this->sanitizeHostName($hostname).'.pub'; | |||
| 			$key_file = $view->fopen($key_filename, "w"); | |||
| 			if ($key_file) { | |||
| 				fwrite($key_file, $this->publicKey); | |||
| 				fclose($key_file); | |||
| 			} else { | |||
| 				\OC::$server->getLogger()->warning('Could not write secret key file.'); | |||
| 			} | |||
| 		} | |||
| 
 | |||
| 		return true; | |||
| 	} | |||
| } | |||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue