220 lines
8.3 KiB
Python

"""passlib.handlers.cisco - Cisco password hashes"""
#=============================================================================
# imports
#=============================================================================
# core
from binascii import hexlify, unhexlify
from hashlib import md5
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import h64, right_pad_string, to_unicode
from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, join_byte_values, \
join_byte_elems, byte_elem_value, iter_byte_values, uascii_to_str, str_to_uascii
import passlib.utils.handlers as uh
# local
__all__ = [
"cisco_pix",
"cisco_type7",
]
#=============================================================================
# cisco pix firewall hash
#=============================================================================
class cisco_pix(uh.HasUserContext, uh.StaticHandler):
"""This class implements the password hash used by Cisco PIX firewalls,
and follows the :ref:`password-hash-api`.
It does a single round of hashing, and relies on the username
as the salt.
The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods
have the following extra keyword:
:type user: str
:param user:
String containing name of user account this password is associated with.
This is *required* in order to correctly hash passwords associated
with a user account on the Cisco device, as it is used to salt
the hash.
Conversely, this *must* be omitted or set to ``""`` in order to correctly
hash passwords which don't have an associated user account
(such as the "enable" password).
"""
#===================================================================
# class attrs
#===================================================================
name = "cisco_pix"
checksum_size = 16
checksum_chars = uh.HASH64_CHARS
#===================================================================
# methods
#===================================================================
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
# XXX: no idea what unicode policy is, but all examples are
# 7-bit ascii compatible, so using UTF-8
secret = secret.encode("utf-8")
user = self.user
if user:
# not positive about this, but it looks like per-user
# accounts use the first 4 chars of the username as the salt,
# whereas global "enable" passwords don't have any salt at all.
if isinstance(user, unicode):
user = user.encode("utf-8")
secret += user[:4]
# null-pad or truncate to 16 bytes
secret = right_pad_string(secret, 16)
# md5 digest
hash = md5(secret).digest()
# drop every 4th byte
hash = join_byte_elems(c for i,c in enumerate(hash) if i & 3 < 3)
# encode using Hash64
return h64.encode_bytes(hash).decode("ascii")
#===================================================================
# eoc
#===================================================================
#=============================================================================
# type 7
#=============================================================================
class cisco_type7(uh.GenericHandler):
"""This class implements the Type 7 password encoding used by Cisco IOS,
and follows the :ref:`password-hash-api`.
It has a simple 4-5 bit salt, but is nonetheless a reversible encoding
instead of a real hash.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genhash` methods
have the following optional keywords:
:type salt: int
:param salt:
This may be an optional salt integer drawn from ``range(0,16)``.
If omitted, one will be chosen at random.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` values that are out of range.
Note that while this class outputs digests in upper-case hexidecimal,
it will accept lower-case as well.
This class also provides the following additional method:
.. automethod:: decode
"""
#===================================================================
# class attrs
#===================================================================
name = "cisco_type7"
setting_kwds = ("salt",)
checksum_chars = uh.UPPER_HEX_CHARS
# NOTE: encoding could handle max_salt_value=99, but since key is only 52
# chars in size, not sure what appropriate behavior is for that edge case.
min_salt_value = 0
max_salt_value = 52
#===================================================================
# methods
#===================================================================
@classmethod
def genconfig(cls):
return None
@classmethod
def genhash(cls, secret, config):
# special case to handle ``config=None`` in same style as StaticHandler
if config is None:
return cls.encrypt(secret)
else:
return super(cisco_type7, cls).genhash(secret, config)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
if len(hash) < 2:
raise uh.exc.InvalidHashError(cls)
salt = int(hash[:2]) # may throw ValueError
return cls(salt=salt, checksum=hash[2:].upper())
def __init__(self, salt=None, **kwds):
super(cisco_type7, self).__init__(**kwds)
self.salt = self._norm_salt(salt)
def _norm_salt(self, salt):
"the salt for this algorithm is an integer 0-52, not a string"
# XXX: not entirely sure that values >15 are valid, so for
# compatibility we don't output those values, but we do accept them.
if salt is None:
if self.use_defaults:
salt = self._generate_salt()
else:
raise TypeError("no salt specified")
if not isinstance(salt, int):
raise uh.exc.ExpectedTypeError(salt, "integer", "salt")
if salt < 0 or salt > self.max_salt_value:
msg = "salt/offset must be in 0..52 range"
if self.relaxed:
warn(msg, uh.PasslibHashWarning)
salt = 0 if salt < 0 else self.max_salt_value
else:
raise ValueError(msg)
return salt
def _generate_salt(self):
return uh.rng.randint(0, 15)
def to_string(self):
return "%02d%s" % (self.salt, uascii_to_str(self.checksum))
def _calc_checksum(self, secret):
# XXX: no idea what unicode policy is, but all examples are
# 7-bit ascii compatible, so using UTF-8
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper()
@classmethod
def decode(cls, hash, encoding="utf-8"):
"""decode hash, returning original password.
:arg hash: encoded password
:param encoding: optional encoding to use (defaults to ``UTF-8``).
:returns: password as unicode
"""
self = cls.from_string(hash)
tmp = unhexlify(self.checksum.encode("ascii"))
raw = self._cipher(tmp, self.salt)
return raw.decode(encoding) if encoding else raw
# type7 uses a xor-based vingere variant, using the following secret key:
_key = u("dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87")
@classmethod
def _cipher(cls, data, salt):
"xor static key against data - encrypts & decrypts"
key = cls._key
key_size = len(key)
return join_byte_values(
value ^ ord(key[(salt + idx) % key_size])
for idx, value in enumerate(iter_byte_values(data))
)
#=============================================================================
# eof
#=============================================================================