codekingpro/portable-devtools
114k
1"""2passlib.handlers.cisco -- Cisco password hashes3"""4#=============================================================================5# imports6#=============================================================================7# core8from binascii import hexlify, unhexlify9from hashlib import md510import logging; log = logging.getLogger(__name__)11from warnings import warn12# site13# pkg14from passlib.utils import right_pad_string, to_unicode, repeat_string, to_bytes15from passlib.utils.binary import h6416from passlib.utils.compat import unicode, u, join_byte_values, \17 join_byte_elems, iter_byte_values, uascii_to_str18import passlib.utils.handlers as uh19# local20__all__ = [21 "cisco_pix",22 "cisco_asa",23 "cisco_type7",24]25 26#=============================================================================27# utils28#=============================================================================29 30#: dummy bytes used by spoil_digest var in cisco_pix._calc_checksum()31_DUMMY_BYTES = b'\xFF' * 3232 33#=============================================================================34# cisco pix firewall hash35#=============================================================================36class cisco_pix(uh.HasUserContext, uh.StaticHandler):37 """38 This class implements the password hash used by older Cisco PIX firewalls,39 and follows the :ref:`password-hash-api`.40 It does a single round of hashing, and relies on the username41 as the salt.42 43 This class only allows passwords <= 16 bytes, anything larger44 will result in a :exc:`~passlib.exc.PasswordSizeError` if passed to :meth:`~cisco_pix.hash`,45 and be silently rejected if passed to :meth:`~cisco_pix.verify`.46 47 The :meth:`~passlib.ifc.PasswordHash.hash`,48 :meth:`~passlib.ifc.PasswordHash.genhash`, and49 :meth:`~passlib.ifc.PasswordHash.verify` methods50 all support the following extra keyword:51 52 :param str user:53 String containing name of user account this password is associated with.54 55 This is *required* in order to correctly hash passwords associated56 with a user account on the Cisco device, as it is used to salt57 the hash.58 59 Conversely, this *must* be omitted or set to ``""`` in order to correctly60 hash passwords which don't have an associated user account61 (such as the "enable" password).62 63 .. versionadded:: 1.664 65 .. versionchanged:: 1.7.166 67 Passwords > 16 bytes are now rejected / throw error instead of being silently truncated,68 to match Cisco behavior. A number of :ref:`bugs <passlib-asa96-bug>` were fixed69 which caused prior releases to generate unverifiable hashes in certain cases.70 """71 #===================================================================72 # class attrs73 #===================================================================74 75 #--------------------76 # PasswordHash77 #--------------------78 name = "cisco_pix"79 80 truncate_size = 1681 82 # NOTE: these are the default policy for PasswordHash,83 # but want to set them explicitly for now.84 truncate_error = True85 truncate_verify_reject = True86 87 #--------------------88 # GenericHandler89 #--------------------90 checksum_size = 1691 checksum_chars = uh.HASH64_CHARS92 93 #--------------------94 # custom95 #--------------------96 97 #: control flag signalling "cisco_asa" mode, set by cisco_asa class98 _is_asa = False99 100 #===================================================================101 # methods102 #===================================================================103 def _calc_checksum(self, secret):104 """105 This function implements the "encrypted" hash format used by Cisco106 PIX & ASA. It's behavior has been confirmed for ASA 9.6,107 but is presumed correct for PIX & other ASA releases,108 as it fits with known test vectors, and existing literature.109 110 While nearly the same, the PIX & ASA hashes have slight differences,111 so this function performs differently based on the _is_asa class flag.112 Noteable changes from PIX to ASA include password size limit113 increased from 16 -> 32, and other internal changes.114 """115 # select PIX vs or ASA mode116 asa = self._is_asa117 118 #119 # encode secret120 #121 # per ASA 8.4 documentation,122 # http://www.cisco.com/c/en/us/td/docs/security/asa/asa84/configuration/guide/asa_84_cli_config/ref_cli.html#Supported_Character_Sets,123 # it supposedly uses UTF-8 -- though some double-encoding issues have124 # been observed when trying to actually *set* a non-ascii password125 # via ASDM, and access via SSH seems to strip 8-bit chars.126 #127 if isinstance(secret, unicode):128 secret = secret.encode("utf-8")129 130 #131 # check if password too large132 #133 # Per ASA 9.6 changes listed in134 # http://www.cisco.com/c/en/us/td/docs/security/asa/roadmap/asa_new_features.html,135 # prior releases had a maximum limit of 32 characters.136 # Testing with an ASA 9.6 system bears this out --137 # setting 32-char password for a user account,138 # and logins will fail if any chars are appended.139 # (ASA 9.6 added new PBKDF2-based hash algorithm,140 # which supports larger passwords).141 #142 # Per PIX documentation143 # http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html,144 # it would not allow passwords > 16 chars.145 #146 # Thus, we unconditionally throw a password size error here,147 # as nothing valid can come from a larger password.148 # NOTE: assuming PIX has same behavior, but at 16 char limit.149 #150 spoil_digest = None151 if len(secret) > self.truncate_size:152 if self.use_defaults:153 # called from hash()154 msg = "Password too long (%s allows at most %d bytes)" % \155 (self.name, self.truncate_size)156 raise uh.exc.PasswordSizeError(self.truncate_size, msg=msg)157 else:158 # called from verify() --159 # We don't want to throw error, or return early,160 # as that would let attacker know too much. Instead, we set a161 # flag to add some dummy data into the md5 digest, so that162 # output won't match truncated version of secret, or anything163 # else that's fixed and predictable.164 spoil_digest = secret + _DUMMY_BYTES165 166 #167 # append user to secret168 #169 # Policy appears to be:170 #171 # * Nothing appended for enable password (user = "")172 #173 # * ASA: If user present, but secret is >= 28 chars, nothing appended.174 #175 # * 1-2 byte users not allowed.176 # DEVIATION: we're letting them through, and repeating their177 # chars ala 3-char user, to simplify testing.178 # Could issue warning in the future though.179 #180 # * 3 byte user has first char repeated, to pad to 4.181 # (observed under ASA 9.6, assuming true elsewhere)182 #183 # * 4 byte users are used directly.184 #185 # * 5+ byte users are truncated to 4 bytes.186 #187 user = self.user188 if user:189 if isinstance(user, unicode):190 user = user.encode("utf-8")191 if not asa or len(secret) < 28:192 secret += repeat_string(user, 4)193 194 #195 # pad / truncate result to limit196 #197 # While PIX always pads to 16 bytes, ASA increases to 32 bytes IFF198 # secret+user > 16 bytes. This makes PIX & ASA have different results199 # where secret size in range(13,16), and user is present --200 # PIX will truncate to 16, ASA will truncate to 32.201 #202 if asa and len(secret) > 16:203 pad_size = 32204 else:205 pad_size = 16206 secret = right_pad_string(secret, pad_size)207 208 #209 # md5 digest210 #211 if spoil_digest:212 # make sure digest won't match truncated version of secret213 secret += spoil_digest214 digest = md5(secret).digest()215 216 #217 # drop every 4th byte218 # NOTE: guessing this was done because it makes output exactly219 # 16 bytes, which may have been a general 'char password[]'220 # size limit under PIX221 #222 digest = join_byte_elems(c for i, c in enumerate(digest) if (i + 1) & 3)223 224 #225 # encode using Hash64226 #227 return h64.encode_bytes(digest).decode("ascii")228 229 # NOTE: works, but needs UTs.230 # @classmethod231 # def same_as_pix(cls, secret, user=""):232 # """233 # test whether (secret + user) combination should234 # have the same hash under PIX and ASA.235 #236 # mainly present to help unittests.237 # """238 # # see _calc_checksum() above for details of this logic.239 # size = len(to_bytes(secret, "utf-8"))240 # if user and size < 28:241 # size += 4242 # return size < 17243 244 #===================================================================245 # eoc246 #===================================================================247 248 249class cisco_asa(cisco_pix):250 """251 This class implements the password hash used by Cisco ASA/PIX 7.0 and newer (2005).252 Aside from a different internal algorithm, it's use and format is identical253 to the older :class:`cisco_pix` class.254 255 For passwords less than 13 characters, this should be identical to :class:`!cisco_pix`,256 but will generate a different hash for most larger inputs257 (See the `Format & Algorithm`_ section for the details).258 259 This class only allows passwords <= 32 bytes, anything larger260 will result in a :exc:`~passlib.exc.PasswordSizeError` if passed to :meth:`~cisco_asa.hash`,261 and be silently rejected if passed to :meth:`~cisco_asa.verify`.262 263 .. versionadded:: 1.7264 265 .. versionchanged:: 1.7.1266 267 Passwords > 32 bytes are now rejected / throw error instead of being silently truncated,268 to match Cisco behavior. A number of :ref:`bugs <passlib-asa96-bug>` were fixed269 which caused prior releases to generate unverifiable hashes in certain cases.270 """271 #===================================================================272 # class attrs273 #===================================================================274 275 #--------------------276 # PasswordHash277 #--------------------278 name = "cisco_asa"279 280 #--------------------281 # TruncateMixin282 #--------------------283 truncate_size = 32284 285 #--------------------286 # cisco_pix287 #--------------------288 _is_asa = True289 290 #===================================================================291 # eoc292 #===================================================================293 294#=============================================================================295# type 7296#=============================================================================297class cisco_type7(uh.GenericHandler):298 """299 This class implements the "Type 7" password encoding used by Cisco IOS,300 and follows the :ref:`password-hash-api`.301 It has a simple 4-5 bit salt, but is nonetheless a reversible encoding302 instead of a real hash.303 304 The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:305 306 :type salt: int307 :param salt:308 This may be an optional salt integer drawn from ``range(0,16)``.309 If omitted, one will be chosen at random.310 311 :type relaxed: bool312 :param relaxed:313 By default, providing an invalid value for one of the other314 keywords will result in a :exc:`ValueError`. If ``relaxed=True``,315 and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`316 will be issued instead. Correctable errors include317 ``salt`` values that are out of range.318 319 Note that while this class outputs digests in upper-case hexadecimal,320 it will accept lower-case as well.321 322 This class also provides the following additional method:323 324 .. automethod:: decode325 """326 #===================================================================327 # class attrs328 #===================================================================329 330 #--------------------331 # PasswordHash332 #--------------------333 name = "cisco_type7"334 setting_kwds = ("salt",)335 336 #--------------------337 # GenericHandler338 #--------------------339 checksum_chars = uh.UPPER_HEX_CHARS340 341 #--------------------342 # HasSalt343 #--------------------344 345 # NOTE: encoding could handle max_salt_value=99, but since key is only 52346 # chars in size, not sure what appropriate behavior is for that edge case.347 min_salt_value = 0348 max_salt_value = 52349 350 #===================================================================351 # methods352 #===================================================================353 @classmethod354 def using(cls, salt=None, **kwds):355 subcls = super(cisco_type7, cls).using(**kwds)356 if salt is not None:357 salt = subcls._norm_salt(salt, relaxed=kwds.get("relaxed"))358 subcls._generate_salt = staticmethod(lambda: salt)359 return subcls360 361 @classmethod362 def from_string(cls, hash):363 hash = to_unicode(hash, "ascii", "hash")364 if len(hash) < 2:365 raise uh.exc.InvalidHashError(cls)366 salt = int(hash[:2]) # may throw ValueError367 return cls(salt=salt, checksum=hash[2:].upper())368 369 def __init__(self, salt=None, **kwds):370 super(cisco_type7, self).__init__(**kwds)371 if salt is not None:372 salt = self._norm_salt(salt)373 elif self.use_defaults:374 salt = self._generate_salt()375 assert self._norm_salt(salt) == salt, "generated invalid salt: %r" % (salt,)376 else:377 raise TypeError("no salt specified")378 self.salt = salt379 380 @classmethod381 def _norm_salt(cls, salt, relaxed=False):382 """383 validate & normalize salt value.384 .. note::385 the salt for this algorithm is an integer 0-52, not a string386 """387 if not isinstance(salt, int):388 raise uh.exc.ExpectedTypeError(salt, "integer", "salt")389 if 0 <= salt <= cls.max_salt_value:390 return salt391 msg = "salt/offset must be in 0..52 range"392 if relaxed:393 warn(msg, uh.PasslibHashWarning)394 return 0 if salt < 0 else cls.max_salt_value395 else:396 raise ValueError(msg)397 398 @staticmethod399 def _generate_salt():400 return uh.rng.randint(0, 15)401 402 def to_string(self):403 return "%02d%s" % (self.salt, uascii_to_str(self.checksum))404 405 def _calc_checksum(self, secret):406 # XXX: no idea what unicode policy is, but all examples are407 # 7-bit ascii compatible, so using UTF-8408 if isinstance(secret, unicode):409 secret = secret.encode("utf-8")410 return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper()411 412 @classmethod413 def decode(cls, hash, encoding="utf-8"):414 """decode hash, returning original password.415 416 :arg hash: encoded password417 :param encoding: optional encoding to use (defaults to ``UTF-8``).418 :returns: password as unicode419 """420 self = cls.from_string(hash)421 tmp = unhexlify(self.checksum.encode("ascii"))422 raw = self._cipher(tmp, self.salt)423 return raw.decode(encoding) if encoding else raw424 425 # type7 uses a xor-based vingere variant, using the following secret key:426 _key = u("dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87")427 428 @classmethod429 def _cipher(cls, data, salt):430 """xor static key against data - encrypts & decrypts"""431 key = cls._key432 key_size = len(key)433 return join_byte_values(434 value ^ ord(key[(salt + idx) % key_size])435 for idx, value in enumerate(iter_byte_values(data))436 )437 438#=============================================================================439# eof440#=============================================================================441 