* @author Morris Jobke * @author Ross Nicoll * * @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 * */ 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; } }