sftp_key.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. <?php
  2. /**
  3. * @author Morris Jobke <hey@morrisjobke.de>
  4. * @author Ross Nicoll <jrn@jrn.me.uk>
  5. *
  6. * @copyright Copyright (c) 2015, ownCloud, Inc.
  7. * @license AGPL-3.0
  8. *
  9. * This code is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License, version 3,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License, version 3,
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>
  20. *
  21. */
  22. namespace OC\Files\Storage;
  23. /**
  24. * Uses phpseclib's Net_SFTP class and the Net_SFTP_Stream stream wrapper to
  25. * provide access to SFTP servers.
  26. */
  27. class SFTP_Key extends \OC\Files\Storage\SFTP {
  28. private $publicKey;
  29. private $privateKey;
  30. public function __construct($params) {
  31. parent::__construct($params);
  32. $this->publicKey = $params['public_key'];
  33. $this->privateKey = $params['private_key'];
  34. }
  35. /**
  36. * Returns the connection.
  37. *
  38. * @return \Net_SFTP connected client instance
  39. * @throws \Exception when the connection failed
  40. */
  41. public function getConnection() {
  42. if (!is_null($this->client)) {
  43. return $this->client;
  44. }
  45. $hostKeys = $this->readHostKeys();
  46. $this->client = new \Net_SFTP($this->getHost());
  47. // The SSH Host Key MUST be verified before login().
  48. $currentHostKey = $this->client->getServerPublicHostKey();
  49. if (array_key_exists($this->getHost(), $hostKeys)) {
  50. if ($hostKeys[$this->getHost()] !== $currentHostKey) {
  51. throw new \Exception('Host public key does not match known key');
  52. }
  53. } else {
  54. $hostKeys[$this->getHost()] = $currentHostKey;
  55. $this->writeHostKeys($hostKeys);
  56. }
  57. $key = $this->getPrivateKey();
  58. if (is_null($key)) {
  59. throw new \Exception('Secret key could not be loaded');
  60. }
  61. if (!$this->client->login($this->getUser(), $key)) {
  62. throw new \Exception('Login failed');
  63. }
  64. return $this->client;
  65. }
  66. /**
  67. * Returns the private key to be used for authentication to the remote server.
  68. *
  69. * @return \Crypt_RSA instance or null in case of a failure to load the key.
  70. */
  71. private function getPrivateKey() {
  72. $key = new \Crypt_RSA();
  73. $key->setPassword(\OC::$server->getConfig()->getSystemValue('secret', ''));
  74. if (!$key->loadKey($this->privateKey)) {
  75. // Should this exception rather than return null?
  76. return null;
  77. }
  78. return $key;
  79. }
  80. /**
  81. * Throws an exception if the provided host name/address is invalid (cannot be resolved
  82. * and is not an IPv4 address).
  83. *
  84. * @return true; never returns in case of a problem, this return value is used just to
  85. * make unit tests happy.
  86. */
  87. public function assertHostAddressValid($hostname) {
  88. // TODO: Should handle IPv6 addresses too
  89. if (!preg_match('/^\d+\.\d+\.\d+\.\d+$/', $hostname) && gethostbyname($hostname) === $hostname) {
  90. // Hostname is not an IPv4 address and cannot be resolved via DNS
  91. throw new \InvalidArgumentException('Cannot resolve hostname.');
  92. }
  93. return true;
  94. }
  95. /**
  96. * Throws an exception if the provided port number is invalid (cannot be resolved
  97. * and is not an IPv4 address).
  98. *
  99. * @return true; never returns in case of a problem, this return value is used just to
  100. * make unit tests happy.
  101. */
  102. public function assertPortNumberValid($port) {
  103. if (!preg_match('/^\d+$/', $port)) {
  104. throw new \InvalidArgumentException('Port number must be a number.');
  105. }
  106. if ($port < 0 || $port > 65535) {
  107. throw new \InvalidArgumentException('Port number must be between 0 and 65535 inclusive.');
  108. }
  109. return true;
  110. }
  111. /**
  112. * Replaces anything that's not an alphanumeric character or "." in a hostname
  113. * with "_", to make it safe for use as part of a file name.
  114. */
  115. protected function sanitizeHostName($name) {
  116. return preg_replace('/[^\d\w\._]/', '_', $name);
  117. }
  118. /**
  119. * Replaces anything that's not an alphanumeric character or "_" in a username
  120. * with "_", to make it safe for use as part of a file name.
  121. */
  122. protected function sanitizeUserName($name) {
  123. return preg_replace('/[^\d\w_]/', '_', $name);
  124. }
  125. public function test() {
  126. // FIXME: Use as expression in empty once PHP 5.4 support is dropped
  127. $host = $this->getHost();
  128. if (empty($host)) {
  129. \OC::$server->getLogger()->warning('Hostname has not been specified');
  130. return false;
  131. }
  132. // FIXME: Use as expression in empty once PHP 5.4 support is dropped
  133. $user = $this->getUser();
  134. if (empty($user)) {
  135. \OC::$server->getLogger()->warning('Username has not been specified');
  136. return false;
  137. }
  138. if (!isset($this->privateKey)) {
  139. \OC::$server->getLogger()->warning('Private key was missing from the request');
  140. return false;
  141. }
  142. // Sanity check the host
  143. $hostParts = explode(':', $this->getHost());
  144. try {
  145. if (count($hostParts) == 1) {
  146. $hostname = $hostParts[0];
  147. $this->assertHostAddressValid($hostname);
  148. } else if (count($hostParts) == 2) {
  149. $hostname = $hostParts[0];
  150. $this->assertHostAddressValid($hostname);
  151. $this->assertPortNumberValid($hostParts[1]);
  152. } else {
  153. throw new \Exception('Host connection string is invalid.');
  154. }
  155. } catch(\Exception $e) {
  156. \OC::$server->getLogger()->warning($e->getMessage());
  157. return false;
  158. }
  159. // Validate the key
  160. $key = $this->getPrivateKey();
  161. if (is_null($key)) {
  162. \OC::$server->getLogger()->warning('Secret key could not be loaded');
  163. return false;
  164. }
  165. try {
  166. if ($this->getConnection()->nlist() === false) {
  167. return false;
  168. }
  169. } catch(\Exception $e) {
  170. // We should be throwing a more specific error, so we're not just catching
  171. // Exception here
  172. \OC::$server->getLogger()->warning($e->getMessage());
  173. return false;
  174. }
  175. // Save the key somewhere it can easily be extracted later
  176. if (\OC::$server->getUserSession()->getUser()) {
  177. $view = new \OC\Files\View('/'.\OC::$server->getUserSession()->getUser()->getUId().'/files_external/sftp_keys');
  178. if (!$view->is_dir('')) {
  179. if (!$view->mkdir('')) {
  180. \OC::$server->getLogger()->warning('Could not create secret key directory.');
  181. return false;
  182. }
  183. }
  184. $key_filename = $this->sanitizeUserName($this->getUser()).'@'.$this->sanitizeHostName($hostname).'.pub';
  185. $key_file = $view->fopen($key_filename, "w");
  186. if ($key_file) {
  187. fwrite($key_file, $this->publicKey);
  188. fclose($key_file);
  189. } else {
  190. \OC::$server->getLogger()->warning('Could not write secret key file.');
  191. }
  192. }
  193. return true;
  194. }
  195. }