diff options
author | Vincent Petry <pvince81@owncloud.com> | 2015-02-10 16:44:29 +0100 |
---|---|---|
committer | Vincent Petry <pvince81@owncloud.com> | 2015-02-10 16:44:29 +0100 |
commit | bd01ff135acb18d5be817ba153adaab9ac431782 (patch) | |
tree | a96fb641999405e4685f1021f00e9196e9e33854 /apps/files_external/lib | |
parent | 5ae03fd650b6f3665d1c69ead674d4f5d6420513 (diff) | |
parent | 64f4f8fc84fd8fc27f0e9e316a2c4c2500c7134f (diff) | |
download | nextcloud-server-bd01ff135acb18d5be817ba153adaab9ac431782.tar.gz nextcloud-server-bd01ff135acb18d5be817ba153adaab9ac431782.zip |
Merge pull request #13190 from is-apps/master-sftp-key
Add SFTP public key authentication support
Diffstat (limited to 'apps/files_external/lib')
-rw-r--r-- | apps/files_external/lib/sftp.php | 21 | ||||
-rw-r--r-- | apps/files_external/lib/sftp_key.php | 194 |
2 files changed, 211 insertions, 4 deletions
diff --git a/apps/files_external/lib/sftp.php b/apps/files_external/lib/sftp.php index f6c56669734..2a762ad068f 100644 --- a/apps/files_external/lib/sftp.php +++ b/apps/files_external/lib/sftp.php @@ -20,7 +20,7 @@ class SFTP extends \OC\Files\Storage\Common { /** * @var \Net_SFTP */ - private $client; + protected $client; private static $tempFiles = array(); @@ -42,7 +42,8 @@ class SFTP extends \OC\Files\Storage\Common { $this->host = substr($this->host, $proto+3); } $this->user = $params['user']; - $this->password = $params['password']; + $this->password + = isset($params['password']) ? $params['password'] : ''; $this->root = isset($params['root']) ? $this->cleanPath($params['root']) : '/'; @@ -101,6 +102,18 @@ class SFTP extends \OC\Files\Storage\Common { return 'sftp::' . $this->user . '@' . $this->host . '/' . $this->root; } + public function getHost() { + return $this->host; + } + + public function getRoot() { + return $this->root; + } + + public function getUser() { + return $this->user; + } + /** * @param string $path */ @@ -121,7 +134,7 @@ class SFTP extends \OC\Files\Storage\Common { return false; } - private function writeHostKeys($keys) { + protected function writeHostKeys($keys) { try { $keyPath = $this->hostKeysPath(); if ($keyPath && file_exists($keyPath)) { @@ -137,7 +150,7 @@ class SFTP extends \OC\Files\Storage\Common { return false; } - private function readHostKeys() { + protected function readHostKeys() { try { $keyPath = $this->hostKeysPath(); if (file_exists($keyPath)) { diff --git a/apps/files_external/lib/sftp_key.php b/apps/files_external/lib/sftp_key.php new file mode 100644 index 00000000000..6113f88a8ff --- /dev/null +++ b/apps/files_external/lib/sftp_key.php @@ -0,0 +1,194 @@ +<?php +/** + * Copyright (c) 2014, 2015 University of Edinburgh <Ross.Nicoll@ed.ac.uk> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OC\Files\Storage; + +/** +* Uses phpseclib's Net_SFTP class and the Net_SFTP_Stream stream wrapper to +* provide access to SFTP servers. +*/ +class SFTP_Key extends \OC\Files\Storage\SFTP { + private $publicKey; + private $privateKey; + + public function __construct($params) { + parent::__construct($params); + $this->publicKey = $params['public_key']; + $this->privateKey = $params['private_key']; + } + + /** + * Returns the connection. + * + * @return \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 \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 \Crypt_RSA instance or null in case of a failure to load the key. + */ + private function getPrivateKey() { + $key = new \Crypt_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() { + if (empty($this->getHost())) { + \OC::$server->getLogger()->warning('Hostname has not been specified'); + return false; + } + if (empty($this->getUser())) { + \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; + } +} |