sftp_key.php 6.5 KB

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