You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sftp_key.php 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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. if (empty($this->getHost())) {
  127. \OC::$server->getLogger()->warning('Hostname has not been specified');
  128. return false;
  129. }
  130. if (empty($this->getUser())) {
  131. \OC::$server->getLogger()->warning('Username has not been specified');
  132. return false;
  133. }
  134. if (!isset($this->privateKey)) {
  135. \OC::$server->getLogger()->warning('Private key was missing from the request');
  136. return false;
  137. }
  138. // Sanity check the host
  139. $hostParts = explode(':', $this->getHost());
  140. try {
  141. if (count($hostParts) == 1) {
  142. $hostname = $hostParts[0];
  143. $this->assertHostAddressValid($hostname);
  144. } else if (count($hostParts) == 2) {
  145. $hostname = $hostParts[0];
  146. $this->assertHostAddressValid($hostname);
  147. $this->assertPortNumberValid($hostParts[1]);
  148. } else {
  149. throw new \Exception('Host connection string is invalid.');
  150. }
  151. } catch(\Exception $e) {
  152. \OC::$server->getLogger()->warning($e->getMessage());
  153. return false;
  154. }
  155. // Validate the key
  156. $key = $this->getPrivateKey();
  157. if (is_null($key)) {
  158. \OC::$server->getLogger()->warning('Secret key could not be loaded');
  159. return false;
  160. }
  161. try {
  162. if ($this->getConnection()->nlist() === false) {
  163. return false;
  164. }
  165. } catch(\Exception $e) {
  166. // We should be throwing a more specific error, so we're not just catching
  167. // Exception here
  168. \OC::$server->getLogger()->warning($e->getMessage());
  169. return false;
  170. }
  171. // Save the key somewhere it can easily be extracted later
  172. if (\OC::$server->getUserSession()->getUser()) {
  173. $view = new \OC\Files\View('/'.\OC::$server->getUserSession()->getUser()->getUId().'/files_external/sftp_keys');
  174. if (!$view->is_dir('')) {
  175. if (!$view->mkdir('')) {
  176. \OC::$server->getLogger()->warning('Could not create secret key directory.');
  177. return false;
  178. }
  179. }
  180. $key_filename = $this->sanitizeUserName($this->getUser()).'@'.$this->sanitizeHostName($hostname).'.pub';
  181. $key_file = $view->fopen($key_filename, "w");
  182. if ($key_file) {
  183. fwrite($key_file, $this->publicKey);
  184. fclose($key_file);
  185. } else {
  186. \OC::$server->getLogger()->warning('Could not write secret key file.');
  187. }
  188. }
  189. return true;
  190. }
  191. }