codekingpro/portable-devtools
114k
1# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com>2#3# This file is part of paramiko.4#5# Paramiko is free software; you can redistribute it and/or modify it under the6# terms of the GNU Lesser General Public License as published by the Free7# Software Foundation; either version 2.1 of the License, or (at your option)8# any later version.9#10# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY11# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR12# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more13# details.14#15# You should have received a copy of the GNU Lesser General Public License16# along with Paramiko; if not, write to the Free Software Foundation, Inc.,17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.18 19 20from base64 import encodebytes, decodebytes21import binascii22import os23import re24 25from collections.abc import MutableMapping26from hashlib import sha127from hmac import HMAC28 29 30from paramiko.pkey import PKey, UnknownKeyType31from paramiko.util import get_logger, constant_time_bytes_eq, b, u32from paramiko.ssh_exception import SSHException33 34 35class HostKeys(MutableMapping):36 """37 Representation of an OpenSSH-style "known hosts" file. Host keys can be38 read from one or more files, and then individual hosts can be looked up to39 verify server keys during SSH negotiation.40 41 A `.HostKeys` object can be treated like a dict; any dict lookup is42 equivalent to calling `lookup`.43 44 .. versionadded:: 1.5.345 """46 47 def __init__(self, filename=None):48 """49 Create a new HostKeys object, optionally loading keys from an OpenSSH50 style host-key file.51 52 :param str filename: filename to load host keys from, or ``None``53 """54 # emulate a dict of { hostname: { keytype: PKey } }55 self._entries = []56 if filename is not None:57 self.load(filename)58 59 def add(self, hostname, keytype, key):60 """61 Add a host key entry to the table. Any existing entry for a62 ``(hostname, keytype)`` pair will be replaced.63 64 :param str hostname: the hostname (or IP) to add65 :param str keytype: key type (``"ssh-rsa"`` or ``"ssh-dss"``)66 :param .PKey key: the key to add67 """68 for e in self._entries:69 if (hostname in e.hostnames) and (e.key.get_name() == keytype):70 e.key = key71 return72 self._entries.append(HostKeyEntry([hostname], key))73 74 def load(self, filename):75 """76 Read a file of known SSH host keys, in the format used by OpenSSH.77 This type of file unfortunately doesn't exist on Windows, but on78 posix, it will usually be stored in79 ``os.path.expanduser("~/.ssh/known_hosts")``.80 81 If this method is called multiple times, the host keys are merged,82 not cleared. So multiple calls to `load` will just call `add`,83 replacing any existing entries and adding new ones.84 85 :param str filename: name of the file to read host keys from86 87 :raises: ``IOError`` -- if there was an error reading the file88 """89 with open(filename, "r") as f:90 for lineno, line in enumerate(f, 1):91 line = line.strip()92 if (len(line) == 0) or (line[0] == "#"):93 continue94 try:95 entry = HostKeyEntry.from_line(line, lineno)96 except SSHException:97 continue98 if entry is not None:99 _hostnames = entry.hostnames100 for h in _hostnames:101 if self.check(h, entry.key):102 entry.hostnames.remove(h)103 if len(entry.hostnames):104 self._entries.append(entry)105 106 def save(self, filename):107 """108 Save host keys into a file, in the format used by OpenSSH. The order109 of keys in the file will be preserved when possible (if these keys were110 loaded from a file originally). The single exception is that combined111 lines will be split into individual key lines, which is arguably a bug.112 113 :param str filename: name of the file to write114 115 :raises: ``IOError`` -- if there was an error writing the file116 117 .. versionadded:: 1.6.1118 """119 with open(filename, "w") as f:120 for e in self._entries:121 line = e.to_line()122 if line:123 f.write(line)124 125 def lookup(self, hostname):126 """127 Find a hostkey entry for a given hostname or IP. If no entry is found,128 ``None`` is returned. Otherwise a dictionary of keytype to key is129 returned. The keytype will be either ``"ssh-rsa"`` or ``"ssh-dss"``.130 131 :param str hostname: the hostname (or IP) to lookup132 :return: dict of `str` -> `.PKey` keys associated with this host133 (or ``None``)134 """135 136 class SubDict(MutableMapping):137 def __init__(self, hostname, entries, hostkeys):138 self._hostname = hostname139 self._entries = entries140 self._hostkeys = hostkeys141 142 def __iter__(self):143 for k in self.keys():144 yield k145 146 def __len__(self):147 return len(self.keys())148 149 def __delitem__(self, key):150 for e in list(self._entries):151 if e.key.get_name() == key:152 self._entries.remove(e)153 break154 else:155 raise KeyError(key)156 157 def __getitem__(self, key):158 for e in self._entries:159 if e.key.get_name() == key:160 return e.key161 raise KeyError(key)162 163 def __setitem__(self, key, val):164 for e in self._entries:165 if e.key is None:166 continue167 if e.key.get_name() == key:168 # replace169 e.key = val170 break171 else:172 # add a new one173 e = HostKeyEntry([hostname], val)174 self._entries.append(e)175 self._hostkeys._entries.append(e)176 177 def keys(self):178 return [179 e.key.get_name()180 for e in self._entries181 if e.key is not None182 ]183 184 entries = []185 for e in self._entries:186 if self._hostname_matches(hostname, e):187 entries.append(e)188 if len(entries) == 0:189 return None190 return SubDict(hostname, entries, self)191 192 def _hostname_matches(self, hostname, entry):193 """194 Tests whether ``hostname`` string matches given SubDict ``entry``.195 196 :returns bool:197 """198 for h in entry.hostnames:199 if (200 h == hostname201 or h.startswith("|1|")202 and not hostname.startswith("|1|")203 and constant_time_bytes_eq(self.hash_host(hostname, h), h)204 ):205 return True206 return False207 208 def check(self, hostname, key):209 """210 Return True if the given key is associated with the given hostname211 in this dictionary.212 213 :param str hostname: hostname (or IP) of the SSH server214 :param .PKey key: the key to check215 :return:216 ``True`` if the key is associated with the hostname; else ``False``217 """218 k = self.lookup(hostname)219 if k is None:220 return False221 host_key = k.get(key.get_name(), None)222 if host_key is None:223 return False224 return host_key.asbytes() == key.asbytes()225 226 def clear(self):227 """228 Remove all host keys from the dictionary.229 """230 self._entries = []231 232 def __iter__(self):233 for k in self.keys():234 yield k235 236 def __len__(self):237 return len(self.keys())238 239 def __getitem__(self, key):240 ret = self.lookup(key)241 if ret is None:242 raise KeyError(key)243 return ret244 245 def __delitem__(self, key):246 index = None247 for i, entry in enumerate(self._entries):248 if self._hostname_matches(key, entry):249 index = i250 break251 if index is None:252 raise KeyError(key)253 self._entries.pop(index)254 255 def __setitem__(self, hostname, entry):256 # don't use this please.257 if len(entry) == 0:258 self._entries.append(HostKeyEntry([hostname], None))259 return260 for key_type in entry.keys():261 found = False262 for e in self._entries:263 if (hostname in e.hostnames) and e.key.get_name() == key_type:264 # replace265 e.key = entry[key_type]266 found = True267 if not found:268 self._entries.append(HostKeyEntry([hostname], entry[key_type]))269 270 def keys(self):271 ret = []272 for e in self._entries:273 for h in e.hostnames:274 if h not in ret:275 ret.append(h)276 return ret277 278 def values(self):279 ret = []280 for k in self.keys():281 ret.append(self.lookup(k))282 return ret283 284 @staticmethod285 def hash_host(hostname, salt=None):286 """287 Return a "hashed" form of the hostname, as used by OpenSSH when storing288 hashed hostnames in the known_hosts file.289 290 :param str hostname: the hostname to hash291 :param str salt: optional salt to use when hashing292 (must be 20 bytes long)293 :return: the hashed hostname as a `str`294 """295 if salt is None:296 salt = os.urandom(sha1().digest_size)297 else:298 if salt.startswith("|1|"):299 salt = salt.split("|")[2]300 salt = decodebytes(b(salt))301 assert len(salt) == sha1().digest_size302 hmac = HMAC(salt, b(hostname), sha1).digest()303 hostkey = "|1|{}|{}".format(u(encodebytes(salt)), u(encodebytes(hmac)))304 return hostkey.replace("\n", "")305 306 307class InvalidHostKey(Exception):308 def __init__(self, line, exc):309 self.line = line310 self.exc = exc311 self.args = (line, exc)312 313 314class HostKeyEntry:315 """316 Representation of a line in an OpenSSH-style "known hosts" file.317 """318 319 def __init__(self, hostnames=None, key=None):320 self.valid = (hostnames is not None) and (key is not None)321 self.hostnames = hostnames322 self.key = key323 324 @classmethod325 def from_line(cls, line, lineno=None):326 """327 Parses the given line of text to find the names for the host,328 the type of key, and the key data. The line is expected to be in the329 format used by the OpenSSH known_hosts file. Fields are separated by a330 single space or tab.331 332 Lines are expected to not have leading or trailing whitespace.333 We don't bother to check for comments or empty lines. All of334 that should be taken care of before sending the line to us.335 336 :param str line: a line from an OpenSSH known_hosts file337 """338 log = get_logger("paramiko.hostkeys")339 fields = re.split(" |\t", line)340 if len(fields) < 3:341 # Bad number of fields342 msg = "Not enough fields found in known_hosts in line {} ({!r})"343 log.info(msg.format(lineno, line))344 return None345 fields = fields[:3]346 347 names, key_type, key = fields348 names = names.split(",")349 350 # Decide what kind of key we're looking at and create an object351 # to hold it accordingly.352 try:353 # TODO: this grew organically and doesn't seem /wrong/ per se (file354 # read -> unicode str -> bytes for base64 decode -> decoded bytes);355 # but in Python 3 forever land, can we simply use356 # `base64.b64decode(str-from-file)` here?357 key_bytes = decodebytes(b(key))358 except binascii.Error as e:359 raise InvalidHostKey(line, e)360 361 try:362 return cls(names, PKey.from_type_string(key_type, key_bytes))363 except UnknownKeyType:364 # TODO 4.0: consider changing HostKeys API so this just raises365 # naturally and the exception is muted higher up in the stack?366 log.info("Unable to handle key of type {}".format(key_type))367 return None368 369 def to_line(self):370 """371 Returns a string in OpenSSH known_hosts file format, or None if372 the object is not in a valid state. A trailing newline is373 included.374 """375 if self.valid:376 return "{} {} {}\n".format(377 ",".join(self.hostnames),378 self.key.get_name(),379 self.key.get_base64(),380 )381 return None382 383 def __repr__(self):384 return "<HostKeyEntry {!r}: {!r}>".format(self.hostnames, self.key)385 