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