1""" 2passlib.handlers.cisco -- Cisco password hashes 3""" 4#============================================================================= 5# imports 6#============================================================================= 7# core 8from binascii import hexlify, unhexlify 9from hashlib import md5 10import logging; log = logging.getLogger(__name__) 11from warnings import warn 12# site 13# pkg 14from passlib.utils import right_pad_string, to_unicode, repeat_string, to_bytes 15from passlib.utils.binary import h64 16from passlib.utils.compat import unicode, u, join_byte_values, \ 17 join_byte_elems, iter_byte_values, uascii_to_str 18import passlib.utils.handlers as uh 19# local 20__all__ = [ 21 "cisco_pix", 22 "cisco_asa", 23 "cisco_type7", 24] 25 26#============================================================================= 27# utils 28#============================================================================= 29 30#: dummy bytes used by spoil_digest var in cisco_pix._calc_checksum() 31_DUMMY_BYTES = b'\xFF' * 32 32 33#============================================================================= 34# cisco pix firewall hash 35#============================================================================= 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 username 41 as the salt. 42 43 This class only allows passwords <= 16 bytes, anything larger 44 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`, and 49 :meth:`~passlib.ifc.PasswordHash.verify` methods 50 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 associated 56 with a user account on the Cisco device, as it is used to salt 57 the hash. 58 59 Conversely, this *must* be omitted or set to ``""`` in order to correctly 60 hash passwords which don't have an associated user account 61 (such as the "enable" password). 62 63 .. versionadded:: 1.6 64 65 .. versionchanged:: 1.7.1 66 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 fixed 69 which caused prior releases to generate unverifiable hashes in certain cases. 70 """ 71 #=================================================================== 72 # class attrs 73 #=================================================================== 74 75 #-------------------- 76 # PasswordHash 77 #-------------------- 78 name = "cisco_pix" 79 80 truncate_size = 16 81 82 # NOTE: these are the default policy for PasswordHash, 83 # but want to set them explicitly for now. 84 truncate_error = True 85 truncate_verify_reject = True 86 87 #-------------------- 88 # GenericHandler 89 #-------------------- 90 checksum_size = 16 91 checksum_chars = uh.HASH64_CHARS 92 93 #-------------------- 94 # custom 95 #-------------------- 96 97 #: control flag signalling "cisco_asa" mode, set by cisco_asa class 98 _is_asa = False 99 100 #=================================================================== 101 # methods 102 #=================================================================== 103 def _calc_checksum(self, secret): 104 """ 105 This function implements the "encrypted" hash format used by Cisco 106 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 limit 113 increased from 16 -> 32, and other internal changes. 114 """ 115 # select PIX vs or ASA mode 116 asa = self._is_asa 117 118 # 119 # encode secret 120 # 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 have 124 # been observed when trying to actually *set* a non-ascii password 125 # 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 large 132 # 133 # Per ASA 9.6 changes listed in 134 # 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 documentation 143 # 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 = None 151 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 a 161 # flag to add some dummy data into the md5 digest, so that 162 # output won't match truncated version of secret, or anything 163 # else that's fixed and predictable. 164 spoil_digest = secret + _DUMMY_BYTES 165 166 # 167 # append user to secret 168 # 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 their 177 # 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.user 188 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 limit 196 # 197 # While PIX always pads to 16 bytes, ASA increases to 32 bytes IFF 198 # secret+user > 16 bytes. This makes PIX & ASA have different results 199 # 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 = 32 204 else: 205 pad_size = 16 206 secret = right_pad_string(secret, pad_size) 207 208 # 209 # md5 digest 210 # 211 if spoil_digest: 212 # make sure digest won't match truncated version of secret 213 secret += spoil_digest 214 digest = md5(secret).digest() 215 216 # 217 # drop every 4th byte 218 # NOTE: guessing this was done because it makes output exactly 219 # 16 bytes, which may have been a general 'char password[]' 220 # size limit under PIX 221 # 222 digest = join_byte_elems(c for i, c in enumerate(digest) if (i + 1) & 3) 223 224 # 225 # encode using Hash64 226 # 227 return h64.encode_bytes(digest).decode("ascii") 228 229 # NOTE: works, but needs UTs. 230 # @classmethod 231 # def same_as_pix(cls, secret, user=""): 232 # """ 233 # test whether (secret + user) combination should 234 # 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 += 4 242 # return size < 17 243 244 #=================================================================== 245 # eoc 246 #=================================================================== 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 identical 253 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 inputs 257 (See the `Format & Algorithm`_ section for the details). 258 259 This class only allows passwords <= 32 bytes, anything larger 260 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.7 264 265 .. versionchanged:: 1.7.1 266 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 fixed 269 which caused prior releases to generate unverifiable hashes in certain cases. 270 """ 271 #=================================================================== 272 # class attrs 273 #=================================================================== 274 275 #-------------------- 276 # PasswordHash 277 #-------------------- 278 name = "cisco_asa" 279 280 #-------------------- 281 # TruncateMixin 282 #-------------------- 283 truncate_size = 32 284 285 #-------------------- 286 # cisco_pix 287 #-------------------- 288 _is_asa = True 289 290 #=================================================================== 291 # eoc 292 #=================================================================== 293 294#============================================================================= 295# type 7 296#============================================================================= 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 encoding 302 instead of a real hash. 303 304 The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: 305 306 :type salt: int 307 :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: bool 312 :param relaxed: 313 By default, providing an invalid value for one of the other 314 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 include 317 ``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:: decode 325 """ 326 #=================================================================== 327 # class attrs 328 #=================================================================== 329 330 #-------------------- 331 # PasswordHash 332 #-------------------- 333 name = "cisco_type7" 334 setting_kwds = ("salt",) 335 336 #-------------------- 337 # GenericHandler 338 #-------------------- 339 checksum_chars = uh.UPPER_HEX_CHARS 340 341 #-------------------- 342 # HasSalt 343 #-------------------- 344 345 # NOTE: encoding could handle max_salt_value=99, but since key is only 52 346 # chars in size, not sure what appropriate behavior is for that edge case. 347 min_salt_value = 0 348 max_salt_value = 52 349 350 #=================================================================== 351 # methods 352 #=================================================================== 353 @classmethod 354 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 subcls 360 361 @classmethod 362 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 ValueError 367 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 = salt 379 380 @classmethod 381 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 string 386 """ 387 if not isinstance(salt, int): 388 raise uh.exc.ExpectedTypeError(salt, "integer", "salt") 389 if 0 <= salt <= cls.max_salt_value: 390 return salt 391 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_value 395 else: 396 raise ValueError(msg) 397 398 @staticmethod 399 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 are 407 # 7-bit ascii compatible, so using UTF-8 408 if isinstance(secret, unicode): 409 secret = secret.encode("utf-8") 410 return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper() 411 412 @classmethod 413 def decode(cls, hash, encoding="utf-8"): 414 """decode hash, returning original password. 415 416 :arg hash: encoded password 417 :param encoding: optional encoding to use (defaults to ``UTF-8``). 418 :returns: password as unicode 419 """ 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 raw 424 425 # type7 uses a xor-based vingere variant, using the following secret key: 426 _key = u("dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87") 427 428 @classmethod 429 def _cipher(cls, data, salt): 430 """xor static key against data - encrypts & decrypts""" 431 key = cls._key 432 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# eof 440#============================================================================= 441