Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
totp.py1909 linesDownload Raw Back to passlib
1"""passlib.totp -- TOTP / RFC6238 / Google Authenticator utilities."""2#=============================================================================3# imports4#=============================================================================5from __future__ import absolute_import, division, print_function6from passlib.utils.compat import PY37# core8import base649import calendar10import json11import logging; log = logging.getLogger(__name__)12import math13import struct14import sys15import time as _time16import re17if PY3:18    from urllib.parse import urlparse, parse_qsl, quote, unquote19else:20    from urllib import quote, unquote21    from urlparse import urlparse, parse_qsl22from warnings import warn23# site24try:25    # TOTP encrypted keys only supported if cryptography (https://cryptography.io) is installed26    from cryptography.hazmat.backends import default_backend as _cg_default_backend27    import cryptography.hazmat.primitives.ciphers.algorithms28    import cryptography.hazmat.primitives.ciphers.modes29    from cryptography.hazmat.primitives import ciphers as _cg_ciphers30    del cryptography31except ImportError:32    log.debug("can't import 'cryptography' package, totp encryption disabled")33    _cg_ciphers = _cg_default_backend = None34# pkg35from passlib import exc36from passlib.exc import TokenError, MalformedTokenError, InvalidTokenError, UsedTokenError37from passlib.utils import (to_unicode, to_bytes, consteq,38                           getrandbytes, rng, SequenceMixin, xor_bytes, getrandstr)39from passlib.utils.binary import BASE64_CHARS, b32encode, b32decode40from passlib.utils.compat import (u, unicode, native_string_types, bascii_to_str, int_types, num_types,41                                  irange, byte_elem_value, UnicodeIO, suppress_cause)42from passlib.utils.decor import hybrid_method, memoized_property43from passlib.crypto.digest import lookup_hash, compile_hmac, pbkdf2_hmac44from passlib.hash import pbkdf2_sha25645# local46__all__ = [47    # frontend classes48    "AppWallet",49    "TOTP",50 51    # errors (defined in passlib.exc, but exposed here for convenience)52    "TokenError",53        "MalformedTokenError",54        "InvalidTokenError",55        "UsedTokenError",56 57    # internal helper classes58    "TotpToken",59    "TotpMatch",60]61 62#=============================================================================63# HACK: python < 2.7.4's urlparse() won't parse query strings unless the url scheme64#       is one of the schemes in the urlparse.uses_query list. 2.7 abandoned65#       this, and parses query if present, regardless of the scheme.66#       as a workaround for older versions, we add "otpauth" to the known list.67#       this was fixed by https://bugs.python.org/issue9374, in 2.7.4 release.68#=============================================================================69if sys.version_info < (2,7,4):70    from urlparse import uses_query71    if "otpauth" not in uses_query:72        uses_query.append("otpauth")73        log.debug("registered 'otpauth' scheme with urlparse.uses_query")74    del uses_query75 76#=============================================================================77# internal helpers78#=============================================================================79 80#-----------------------------------------------------------------------------81# token parsing / rendering helpers82#-----------------------------------------------------------------------------83 84#: regex used to clean whitespace from tokens & keys85_clean_re = re.compile(u(r"\s|[-=]"), re.U)86 87_chunk_sizes = [4,6,5]88 89def _get_group_size(klen):90    """91    helper for group_string() --92    calculates optimal size of group for given string size.93    """94    # look for exact divisor95    for size in _chunk_sizes:96        if not klen % size:97            return size98    # fallback to divisor with largest remainder99    # (so chunks are as close to even as possible)100    best = _chunk_sizes[0]101    rem = 0102    for size in _chunk_sizes:103        if klen % size > rem:104            best = size105            rem = klen % size106    return best107 108def group_string(value, sep="-"):109    """110    reformat string into (roughly) evenly-sized groups, separated by **sep**.111    useful for making tokens & keys easier to read by humans.112    """113    klen = len(value)114    size = _get_group_size(klen)115    return sep.join(value[o:o+size] for o in irange(0, klen, size))116 117#-----------------------------------------------------------------------------118# encoding helpers119#-----------------------------------------------------------------------------120 121def _decode_bytes(key, format):122    """123    internal TOTP() helper --124    decodes key according to specified format.125    """126    if format == "raw":127        if not isinstance(key, bytes):128            raise exc.ExpectedTypeError(key, "bytes", "key")129        return key130    # for encoded data, key must be either unicode or ascii-encoded bytes,131    # and must contain a hex or base32 string.132    key = to_unicode(key, param="key")133    key = _clean_re.sub("", key).encode("utf-8") # strip whitespace & hypens134    if format == "hex" or format == "base16":135        return base64.b16decode(key.upper())136    elif format == "base32":137        return b32decode(key)138    # XXX: add base64 support?139    else:140        raise ValueError("unknown byte-encoding format: %r" % (format,))141 142#=============================================================================143# OTP management144#=============================================================================145 146#: flag for detecting if encrypted totp support is present147AES_SUPPORT = bool(_cg_ciphers)148 149#: regex for validating secret tags150_tag_re = re.compile("(?i)^[a-z0-9][a-z0-9_.-]*$")151 152class AppWallet(object):153    """154    This class stores application-wide secrets that can be used155    to encrypt & decrypt TOTP keys for storage.156    It's mostly an internal detail, applications usually just need157    to pass ``secrets`` or ``secrets_path`` to :meth:`TOTP.using`.158 159    .. seealso::160 161        :ref:`totp-storing-instances` for more details on this workflow.162 163    Arguments164    =========165    :param secrets:166        Dict of application secrets to use when encrypting/decrypting167        stored TOTP keys.  This should include a secret to use when encrypting168        new keys, but may contain additional older secrets to decrypt169        existing stored keys.170 171        The dict should map tags -> secrets, so that each secret is identified172        by a unique tag.  This tag will be stored along with the encrypted173        key in order to determine which secret should be used for decryption.174        Tag should be string that starts with regex range ``[a-z0-9]``,175        and the remaining characters must be in ``[a-z0-9_.-]``.176 177        It is recommended to use something like a incremental counter178        ("1", "2", ...), an ISO date ("2016-01-01", "2016-05-16", ...), 179        or a timestamp ("19803495", "19813495", ...) when assigning tags.180 181        This mapping be provided in three formats:182 183        * A python dict mapping tag -> secret184        * A JSON-formatted string containing the dict185        * A multiline string with the format ``"tag: value\\ntag: value\\n..."``186 187        (This last format is mainly useful when loading from a text file via **secrets_path**)188 189        .. seealso:: :func:`generate_secret` to create a secret with sufficient entropy190 191    :param secrets_path:192        Alternately, callers can specify a separate file where the193        application-wide secrets are stored, using either of the string194        formats described in **secrets**.195 196    :param default_tag:197        Specifies which tag in **secrets** should be used as the default198        for encrypting new keys. If omitted, the tags will be sorted,199        and the largest tag used as the default.200 201        if all tags are numeric, they will be sorted numerically;202        otherwise they will be sorted alphabetically.203        this permits tags to be assigned numerically,204        or e.g. using ``YYYY-MM-DD`` dates.205 206    :param encrypt_cost:207        Optional time-cost factor for key encryption.208        This value corresponds to log2() of the number of PBKDF2209        rounds used.210 211    .. warning::212 213        The application secret(s) should be stored in a secure location by214        your application, and each secret should contain a large amount215        of entropy (to prevent brute-force attacks if the encrypted keys216        are leaked).217 218        :func:`generate_secret` is provided as a convenience helper219        to generate a new application secret of suitable size.220 221        Best practice is to load these values from a file via **secrets_path**,222        and then have your application give up permission to read this file223        once it's running.224 225    Public Methods226    ==============227    .. autoattribute:: has_secrets228    .. autoattribute:: default_tag229 230    Semi-Private Methods231    ====================232    The following methods are used internally by the :class:`TOTP`233    class in order to encrypt & decrypt keys using the provided application234    secrets.  They will generally not be publically useful, and may have their235    API changed periodically.236 237    .. automethod:: get_secret238    .. automethod:: encrypt_key239    .. automethod:: decrypt_key240    """241    #========================================================================242    # instance attrs243    #========================================================================244 245    #: default salt size for encrypt_key() output246    salt_size = 12247 248    #: default cost (log2 of pbkdf2 rounds) for encrypt_key() output249    #: NOTE: this is relatively low, since the majority of the security250    #: relies on a high entropy secret to pass to AES.251    encrypt_cost = 14252 253    #: map of secret tag -> secret bytes254    _secrets = None255 256    #: tag for default secret257    default_tag = None258 259    #========================================================================260    # init261    #========================================================================262    def __init__(self, secrets=None, default_tag=None, encrypt_cost=None,263                 secrets_path=None):264 265        # TODO: allow a lot more things to be customized from here,266        #       e.g. setting default TOTP constructor options.267 268        #269        # init cost270        #271        if encrypt_cost is not None:272            if isinstance(encrypt_cost, native_string_types):273                encrypt_cost = int(encrypt_cost)274            assert encrypt_cost >= 0275            self.encrypt_cost = encrypt_cost276 277        #278        # init secrets map279        #280 281        # load secrets from file (if needed)282        if secrets_path is not None:283            if secrets is not None:284                raise TypeError("'secrets' and 'secrets_path' are mutually exclusive")285            secrets = open(secrets_path, "rt").read()286 287        # parse & store secrets288        secrets = self._secrets = self._parse_secrets(secrets)289 290        #291        # init default tag/secret292        #293        if secrets:294            if default_tag is not None:295                # verify that tag is present in map296                self.get_secret(default_tag)297            elif all(tag.isdigit() for tag in secrets):298                default_tag = max(secrets, key=int)299            else:300                default_tag = max(secrets)301            self.default_tag = default_tag302 303    def _parse_secrets(self, source):304        """305        parse 'secrets' parameter306 307        :returns:308            Dict[tag:str, secret:bytes]309        """310        # parse string formats311        # to make this easy to pass in configuration from a separate file,312        # 'secrets' can be string using two formats -- json & "tag:value\n"313        check_type = True314        if isinstance(source, native_string_types):315            if source.lstrip().startswith(("[", "{")):316                # json list / dict317                source = json.loads(source)318            elif "\n" in source and ":" in source:319                # multiline string containing series of "tag: value\n" rows;320                # empty and "#\n" rows are ignored321                def iter_pairs(source):322                    for line in source.splitlines():323                        line = line.strip()324                        if line and not line.startswith("#"):325                            tag, secret = line.split(":", 1)326                            yield tag.strip(), secret.strip()327                source = iter_pairs(source)328                check_type = False329            else:330                raise ValueError("unrecognized secrets string format")331 332        # ensure we have iterable of (tag, value) pairs333        if source is None:334            return {}335        elif isinstance(source, dict):336            source = source.items()337        # XXX: could support iterable of (tag,value) pairs, but not yet needed...338        # elif check_type and (isinstance(source, str) or not isinstance(source, Iterable)):339        elif check_type:340            raise TypeError("'secrets' must be mapping, or list of items")341 342        # parse into final dict, normalizing contents343        return dict(self._parse_secret_pair(tag, value)344                    for tag, value in source)345 346    def _parse_secret_pair(self, tag, value):347        if isinstance(tag, native_string_types):348            pass349        elif isinstance(tag, int):350            tag = str(tag)351        else:352            raise TypeError("tag must be unicode/string: %r" % (tag,))353        if not _tag_re.match(tag):354            raise ValueError("tag contains invalid characters: %r" % (tag,))355        if not isinstance(value, bytes):356            value = to_bytes(value, param="secret %r" % (tag,))357        if not value:358            raise ValueError("tag contains empty secret: %r" % (tag,))359        return tag, value360 361    #========================================================================362    # accessing secrets363    #========================================================================364 365    @property366    def has_secrets(self):367        """whether at least one application secret is present"""368        return self.default_tag is not None369 370    def get_secret(self, tag):371        """372        resolve a secret tag to the secret (as bytes).373        throws a KeyError if not found.374        """375        secrets = self._secrets376        if not secrets:377            raise KeyError("no application secrets configured")378        try:379            return secrets[tag]380        except KeyError:381            raise suppress_cause(KeyError("unknown secret tag: %r" % (tag,)))382 383    #========================================================================384    # encrypted key helpers -- used internally by TOTP385    #========================================================================386 387    @staticmethod388    def _cipher_aes_key(value, secret, salt, cost, decrypt=False):389        """390        Internal helper for :meth:`encrypt_key` --391        handles lowlevel encryption/decryption.392 393        Algorithm details:394 395        This function uses PBKDF2-HMAC-SHA256 to generate a 32-byte AES key396        and a 16-byte IV from the application secret & random salt.397        It then uses AES-256-CTR to encrypt/decrypt the TOTP key.398 399        CTR mode was chosen over CBC because the main attack scenario here400        is that the attacker has stolen the database, and is trying to decrypt a TOTP key401        (the plaintext value here).  To make it hard for them, we want every password402        to decrypt to a potentially valid key -- thus need to avoid any authentication403        or padding oracle attacks.  While some random padding construction could be devised404        to make this work for CBC mode, a stream cipher mode is just plain simpler.405        OFB/CFB modes would also work here, but seeing as they have malleability406        and cyclic issues (though remote and barely relevant here),407        CTR was picked as the best overall choice.408        """409        # make sure backend AES support is available410        if _cg_ciphers is None:411            raise RuntimeError("TOTP encryption requires 'cryptography' package "412                               "(https://cryptography.io)")413 414        # use pbkdf2 to derive both key (32 bytes) & iv (16 bytes)415        # NOTE: this requires 2 sha256 blocks to be calculated.416        keyiv = pbkdf2_hmac("sha256", secret, salt=salt, rounds=(1 << cost), keylen=48)417 418        # use AES-256-CTR to encrypt/decrypt input value419        cipher = _cg_ciphers.Cipher(_cg_ciphers.algorithms.AES(keyiv[:32]),420                                    _cg_ciphers.modes.CTR(keyiv[32:]),421                                    _cg_default_backend())422        ctx = cipher.decryptor() if decrypt else cipher.encryptor()423        return ctx.update(value) + ctx.finalize()424 425    def encrypt_key(self, key):426        """427        Helper used to encrypt TOTP keys for storage.428 429        :param key:430            TOTP key to encrypt, as raw bytes.431 432        :returns:433            dict containing encrypted TOTP key & configuration parameters.434            this format should be treated as opaque, and potentially subject435            to change, though it is designed to be easily serialized/deserialized436            (e.g. via JSON).437 438        .. note::439 440            This function requires installation of the external441            `cryptography <https://cryptography.io>`_ package.442 443        To give some algorithm details:  This function uses AES-256-CTR to encrypt444        the provided data.  It takes the application secret and randomly generated salt,445        and uses PBKDF2-HMAC-SHA256 to combine them and generate the AES key & IV.446        """447        if not key:448            raise ValueError("no key provided")449        salt = getrandbytes(rng, self.salt_size)450        cost = self.encrypt_cost451        tag = self.default_tag452        if not tag:453            raise TypeError("no application secrets configured, can't encrypt OTP key")454        ckey = self._cipher_aes_key(key, self.get_secret(tag), salt, cost)455        # XXX: switch to base64?456        return dict(v=1, c=cost, t=tag, s=b32encode(salt), k=b32encode(ckey))457 458    def decrypt_key(self, enckey):459        """460        Helper used to decrypt TOTP keys from storage format.461        Consults configured secrets to decrypt key.462 463        :param source:464            source object, as returned by :meth:`encrypt_key`.465 466        :returns:467            ``(key, needs_recrypt)`` --468 469            **key** will be the decrypted key, as bytes.470 471            **needs_recrypt** will be a boolean flag indicating472            whether encryption cost or default tag is too old,473            and henace that key needs re-encrypting before storing.474 475        .. note::476 477            This function requires installation of the external478            `cryptography <https://cryptography.io>`_ package.479        """480        if not isinstance(enckey, dict):481            raise TypeError("'enckey' must be dictionary")482        version = enckey.get("v", None)483        needs_recrypt = False484        if version == 1:485            _cipher_key = self._cipher_aes_key486        else:487            raise ValueError("missing / unrecognized 'enckey' version: %r" % (version,))488        tag = enckey['t']489        cost = enckey['c']490        key = _cipher_key(491            value=b32decode(enckey['k']),492            secret=self.get_secret(tag),493            salt=b32decode(enckey['s']),494            cost=cost,495        )496        if cost != self.encrypt_cost or tag != self.default_tag:497            needs_recrypt = True498        return key, needs_recrypt499 500    #=============================================================================501    # eoc502    #=============================================================================503 504#=============================================================================505# TOTP class506#=============================================================================507 508#: helper to convert HOTP counter to bytes509_pack_uint64 = struct.Struct(">Q").pack510 511#: helper to extract value from HOTP digest512_unpack_uint32 = struct.Struct(">I").unpack513 514#: dummy bytes used as temp key for .using() method515_DUMMY_KEY = b"\x00" * 16516 517class TOTP(object):518    """519    Helper for generating and verifying TOTP codes.520 521    Given a secret key and set of configuration options, this object522    offers methods for token generation, token validation, and serialization.523    It can also be used to track important persistent TOTP state,524    such as the last counter used.525 526    This class accepts the following options527    (only **key** and **format** may be specified as positional arguments).528 529    :arg str key:530        The secret key to use. By default, should be encoded as531        a base32 string (see **format** for other encodings).532 533        Exactly one of **key** or ``new=True`` must be specified.534 535    :arg str format:536        The encoding used by the **key** parameter. May be one of:537        ``"base32"`` (base32-encoded string),538        ``"hex"`` (hexadecimal string), or ``"raw"`` (raw bytes).539        Defaults to ``"base32"``.540 541    :param bool new:542        If ``True``, a new key will be generated using :class:`random.SystemRandom`.543 544        Exactly one ``new=True`` or **key** must be specified.545 546    :param str label:547        Label to associate with this token when generating a URI.548        Displayed to user by most OTP client applications (e.g. Google Authenticator),549        and typically has format such as ``"John Smith"`` or ``"jsmith@webservice.example.org"``.550        Defaults to ``None``.551        See :meth:`to_uri` for details.552 553    :param str issuer:554        String identifying the token issuer (e.g. the domain name of your service).555        Used internally by some OTP client applications (e.g. Google Authenticator) to distinguish entries556        which otherwise have the same label.557        Optional but strongly recommended if you're rendering to a URI.558        Defaults to ``None``.559        See :meth:`to_uri` for details.560 561    :param int size:562        Number of bytes when generating new keys. Defaults to size of hash algorithm (e.g. 20 for SHA1).563 564        .. warning::565 566            Overriding the default values for ``digits``, ``period``, or ``alg`` may567            cause problems with some OTP client programs (such as Google Authenticator),568            which may have these defaults hardcoded.569 570    :param int digits:571        The number of digits in the generated / accepted tokens. Defaults to ``6``.572        Must be in range [6 .. 10].573 574        .. rst-class:: inline-title575        .. caution::576           Due to a limitation of the HOTP algorithm, the 10th digit can only take on values 0 .. 2,577           and thus offers very little extra security.578 579    :param str alg:580        Name of hash algorithm to use. Defaults to ``"sha1"``.581        ``"sha256"`` and ``"sha512"`` are also accepted, per :rfc:`6238`.582 583    :param int period:584        The time-step period to use, in integer seconds. Defaults to ``30``.585 586    ..587        See the passlib documentation for a full list of attributes & methods.588    """589    #=============================================================================590    # class attrs591    #=============================================================================592 593    #: minimum number of bytes to allow in key, enforced by passlib.594    # XXX: see if spec says anything relevant to this.595    _min_key_size = 10596 597    #: minimum & current serialization version (may be set independently by subclasses)598    min_json_version = json_version = 1599 600    #: AppWallet that this class will use for encrypting/decrypting keys.601    #: (can be overwritten via the :meth:`TOTP.using()` constructor)602    wallet = None603 604    #: function to get system time in seconds, as needed by :meth:`generate` and :meth:`verify`.605    #: defaults to :func:`time.time`, but can be overridden on a per-instance basis.606    now = _time.time607 608    #=============================================================================609    # instance attrs610    #=============================================================================611 612    #---------------------------------------------------------------------------613    # configuration attrs614    #---------------------------------------------------------------------------615 616    #: [private] secret key as raw :class:`!bytes`617    #: see .key property for public access.618    _key = None619 620    #: [private] cached copy of encrypted secret,621    #: so .to_json() doesn't have to re-encrypt on each call.622    _encrypted_key = None623 624    #: [private] cached copy of keyed HMAC function,625    #: so ._generate() doesn't have to rebuild this each time626    #: ._find_match() invokes it.627    _keyed_hmac = None628 629    #: number of digits in the generated tokens.630    digits = 6631 632    #: name of hash algorithm in use (e.g. ``"sha1"``)633    alg = "sha1"634 635    #: default label for :meth:`to_uri`636    label = None637 638    #: default issuer for :meth:`to_uri`639    issuer = None640 641    #: number of seconds per counter step.642    #: *(TOTP uses an internal time-derived counter which643    #: increments by 1 every* :attr:`!period` *seconds)*.644    period = 30645 646    #---------------------------------------------------------------------------647    # state attrs648    #---------------------------------------------------------------------------649 650    #: Flag set by deserialization methods to indicate the object needs to be re-serialized.651    #: This can be for a number of reasons -- encoded using deprecated format,652    #: or encrypted using a deprecated key or too few rounds.653    changed = False654 655    #=============================================================================656    # prototype construction657    #=============================================================================658    @classmethod659    def using(cls, digits=None, alg=None, period=None,660              issuer=None, wallet=None, now=None, **kwds):661        """662        Dynamically create subtype of :class:`!TOTP` class663        which has the specified defaults set.664 665        :parameters: **digits, alg, period, issuer**:666 667            All these options are the same as in the :class:`TOTP` constructor,668            and the resulting class will use any values you specify here669            as the default for all TOTP instances it creates.670 671        :param wallet:672            Optional :class:`AppWallet` that will be used for encrypting/decrypting keys.673 674        :param secrets, secrets_path, encrypt_cost:675 676            If specified, these options will be passed to the :class:`AppWallet` constructor,677            allowing you to directly specify the secret keys that should be used678            to encrypt & decrypt stored keys.679 680        :returns:681            subclass of :class:`!TOTP`.682 683        This method is useful for creating a TOTP class configured684        to use your application's secrets for encrypting & decrypting685        keys, as well as create new keys using it's desired configuration defaults.686 687        As an example::688 689            >>> # your application can create a custom class when it initializes690            >>> from passlib.totp import TOTP, generate_secret691            >>> TotpFactory = TOTP.using(secrets={"1": generate_secret()})692 693            >>> # subsequent TOTP objects created from this factory694            >>> # will use the specified secrets to encrypt their keys...695            >>> totp = TotpFactory.new()696            >>> totp.to_dict()697            {'enckey': {'c': 14,698              'k': 'H77SYXWORDPGVOQTFRR2HFUB3C45XXI7',699              's': 'G5DOQPIHIBUM2OOHHADQ',700              't': '1',701              'v': 1},702             'type': 'totp',703             'v': 1}704 705        .. seealso:: :ref:`totp-creation` and :ref:`totp-storing-instances` tutorials for a usage example706        """707        # XXX: could add support for setting default match 'window' and 'reuse' policy708 709        # :param now:710        #     Optional callable that should return current time for generator to use.711        #     Default to :func:`time.time`. This optional is generally not needed,712        #     and is mainly present for examples & unit-testing.713 714        subcls = type("TOTP", (cls,), {})715 716        def norm_param(attr, value):717            """718            helper which uses constructor to validate parameter value.719            it returns corresponding attribute, so we use normalized value.720            """721            # NOTE: this creates *subclass* instance,722            #       so normalization takes into account any custom params723            #       already stored.724            kwds = dict(key=_DUMMY_KEY, format="raw")725            kwds[attr] = value726            obj = subcls(**kwds)727            return getattr(obj, attr)728 729        if digits is not None:730            subcls.digits = norm_param("digits", digits)731 732        if alg is not None:733            subcls.alg = norm_param("alg", alg)734 735        if period is not None:736            subcls.period = norm_param("period", period)737 738        # XXX: add default size as configurable parameter?739 740        if issuer is not None:741            subcls.issuer = norm_param("issuer", issuer)742 743        if kwds:744            subcls.wallet = AppWallet(**kwds)745            if wallet:746                raise TypeError("'wallet' and 'secrets' keywords are mutually exclusive")747        elif wallet is not None:748            if not isinstance(wallet, AppWallet):749                raise exc.ExpectedTypeError(wallet, AppWallet, "wallet")750            subcls.wallet = wallet751 752        if now is not None:753            assert isinstance(now(), num_types) and now() >= 0, \754                "now() function must return non-negative int/float"755            subcls.now = staticmethod(now)756 757        return subcls758 759    #=============================================================================760    # init761    #=============================================================================762 763    @classmethod764    def new(cls, **kwds):765        """766        convenience alias for creating new TOTP key, same as ``TOTP(new=True)``767        """768        return cls(new=True, **kwds)769 770    def __init__(self, key=None, format="base32",771                 # keyword only...772                 new=False, digits=None, alg=None, size=None, period=None,773                 label=None, issuer=None, changed=False,774                 **kwds):775        super(TOTP, self).__init__(**kwds)776        if changed:777            self.changed = changed778 779        # validate & normalize alg780        info = lookup_hash(alg or self.alg)781        self.alg = info.name782        digest_size = info.digest_size783        if digest_size < 4:784            raise RuntimeError("%r hash digest too small" % alg)785 786        # parse or generate new key787        if new:788            # generate new key789            if key:790                raise TypeError("'key' and 'new=True' are mutually exclusive")791            if size is None:792                # default to digest size, per RFC 6238 Section 5.1793                size = digest_size794            elif size > digest_size:795                # not forbidden by spec, but would just be wasted bytes.796                # maybe just warn about this?797                raise ValueError("'size' should be less than digest size "798                                 "(%d)" % digest_size)799            self.key = getrandbytes(rng, size)800        elif not key:801            raise TypeError("must specify either an existing 'key', or 'new=True'")802        elif format == "encrypted":803            # NOTE: this handles decrypting & setting '.key'804            self.encrypted_key = key805        elif key:806            # use existing key, encoded using specified <format>807            self.key = _decode_bytes(key, format)808 809        # enforce min key size810        if len(self.key) < self._min_key_size:811            # only making this fatal for new=True,812            # so that existing (but ridiculously small) keys can still be used.813            msg = "for security purposes, secret key must be >= %d bytes" % self._min_key_size814            if new:815                raise ValueError(msg)816            else:817                warn(msg, exc.PasslibSecurityWarning, stacklevel=1)818 819        # validate digits820        if digits is None:821            digits = self.digits822        if not isinstance(digits, int_types):823            raise TypeError("digits must be an integer, not a %r" % type(digits))824        if digits < 6 or digits > 10:825            raise ValueError("digits must in range(6,11)")826        self.digits = digits827 828        # validate label829        if label:830            self._check_label(label)831            self.label = label832 833        # validate issuer834        if issuer:835            self._check_issuer(issuer)836            self.issuer = issuer837 838        # init period839        if period is not None:840            self._check_serial(period, "period", minval=1)841            self.period = period842 843    #=============================================================================844    # helpers to verify value types & ranges845    #=============================================================================846 847    @staticmethod848    def _check_serial(value, param, minval=0):849        """850        check that serial value (e.g. 'counter') is non-negative integer851        """852        if not isinstance(value, int_types):853            raise exc.ExpectedTypeError(value, "int", param)854        if value < minval:855            raise ValueError("%s must be >= %d" % (param, minval))856 857    @staticmethod858    def _check_label(label):859        """860        check that label doesn't contain chars forbidden by KeyURI spec861        """862        if label and ":" in label:863            raise ValueError("label may not contain ':'")864 865    @staticmethod866    def _check_issuer(issuer):867        """868        check that issuer doesn't contain chars forbidden by KeyURI spec869        """870        if issuer and ":" in issuer:871            raise ValueError("issuer may not contain ':'")872 873    #=============================================================================874    # key attributes875    #=============================================================================876 877    #------------------------------------------------------------------878    # raw key879    #------------------------------------------------------------------880    @property881    def key(self):882        """883        secret key as raw bytes884        """885        return self._key886 887    @key.setter888    def key(self, value):889        # set key890        if not isinstance(value, bytes):891            raise exc.ExpectedTypeError(value, bytes, "key")892        self._key = value893 894        # clear cached properties derived from key895        self._encrypted_key = self._keyed_hmac = None896 897    #------------------------------------------------------------------898    # encrypted key899    #------------------------------------------------------------------900    @property901    def encrypted_key(self):902        """903        secret key, encrypted using application secret.904        this match the output of :meth:`AppWallet.encrypt_key`,905        and should be treated as an opaque json serializable object.906        """907        enckey = self._encrypted_key908        if enckey is None:909            wallet = self.wallet910            if not wallet:911                raise TypeError("no application secrets present, can't encrypt TOTP key")912            enckey = self._encrypted_key = wallet.encrypt_key(self.key)913        return enckey914 915    @encrypted_key.setter916    def encrypted_key(self, value):917        wallet = self.wallet918        if not wallet:919            raise TypeError("no application secrets present, can't decrypt TOTP key")920        self.key, needs_recrypt = wallet.decrypt_key(value)921        if needs_recrypt:922            # mark as changed so it gets re-encrypted & written to db923            self.changed = True924        else:925            # cache encrypted key for re-use926            self._encrypted_key = value927 928    #------------------------------------------------------------------929    # pretty-printed / encoded key helpers930    #------------------------------------------------------------------931 932    @property933    def hex_key(self):934        """935        secret key encoded as hexadecimal string936        """937        return bascii_to_str(base64.b16encode(self.key)).lower()938 939    @property940    def base32_key(self):941        """942        secret key encoded as base32 string943        """944        return b32encode(self.key)945 946    def pretty_key(self, format="base32", sep="-"):947        """948        pretty-print the secret key.949 950        This is mainly useful for situations where the user cannot get the qrcode to work,951        and must enter the key manually into their TOTP client. It tries to format952        the key in a manner that is easier for humans to read.953 954        :param format:955            format to output secret key. ``"hex"`` and ``"base32"`` are both accepted.956 957        :param sep:958            separator to insert to break up key visually.959            can be any of ``"-"`` (the default), ``" "``, or ``False`` (no separator).960 961        :return:962            key as native string.963 964        Usage example::965 966            >>> t = TOTP('s3jdvb7qd2r7jpxx')967            >>> t.pretty_key()968            'S3JD-VB7Q-D2R7-JPXX'969        """970        if format == "hex" or format == "base16":971            key = self.hex_key972        elif format == "base32":973            key = self.base32_key974        else:975            raise ValueError("unknown byte-encoding format: %r" % (format,))976        if sep:977            key = group_string(key, sep)978        return key979 980    #=============================================================================981    # time & token parsing982    #=============================================================================983 984    @classmethod985    def normalize_time(cls, time):986        """987        Normalize time value to unix epoch seconds.988 989        :arg time:990            Can be ``None``, :class:`!datetime`,991            or unix epoch timestamp as :class:`!float` or :class:`!int`.992            If ``None``, uses current system time.993            Naive datetimes are treated as UTC.994 995        :returns:996            unix epoch timestamp as :class:`int`.997        """998        if isinstance(time, int_types):999            return time1000        elif isinstance(time, float):1001            return int(time)1002        elif time is None:1003            return int(cls.now())1004        elif hasattr(time, "utctimetuple"):1005            # coerce datetime to UTC timestamp1006            # NOTE: utctimetuple() assumes naive datetimes are in UTC1007            # NOTE: we explicitly *don't* want microseconds.1008            return calendar.timegm(time.utctimetuple())1009        else:1010            raise exc.ExpectedTypeError(time, "int, float, or datetime", "time")1011 1012    def _time_to_counter(self, time):1013        """1014        convert timestamp to HOTP counter using :attr:`period`.1015        """1016        return time // self.period1017 1018    def _counter_to_time(self, counter):1019        """1020        convert HOTP counter to timestamp using :attr:`period`.1021        """1022        return counter * self.period1023 1024    @hybrid_method1025    def normalize_token(self_or_cls, token):1026        """1027        Normalize OTP token representation:1028        strips whitespace, converts integers to a zero-padded string,1029        validates token content & number of digits.1030 1031        This is a hybrid method -- it can be called at the class level,1032        as ``TOTP.normalize_token()``, or the instance level as ``TOTP().normalize_token()``.1033        It will normalize to the instance-specific number of :attr:`~TOTP.digits`,1034        or use the class default.1035 1036        :arg token:1037            token as ascii bytes, unicode, or an integer.1038 1039        :raises ValueError:1040            if token has wrong number of digits, or contains non-numeric characters.1041 1042        :returns:1043            token as :class:`!unicode` string, containing only digits 0-9.1044        """1045        digits = self_or_cls.digits1046        if isinstance(token, int_types):1047            token = u("%0*d") % (digits, token)1048        else:1049            token = to_unicode(token, param="token")1050            token = _clean_re.sub(u(""), token)1051            if not token.isdigit():1052                raise MalformedTokenError("Token must contain only the digits 0-9")1053        if len(token) != digits:1054            raise MalformedTokenError("Token must have exactly %d digits" % digits)1055        return token1056 1057    #=============================================================================1058    # token generation1059    #=============================================================================1060 1061# # debug helper1062#    def generate_range(self, size, time=None):1063#        counter = self._time_to_counter(time) - (size + 1) // 21064#        end = counter + size1065#        while counter <= end:1066#            token = self._generate(counter)1067#            yield TotpToken(self, token, counter)1068#            counter += 11069 1070    def generate(self, time=None):1071        """1072        Generate token for specified time1073        (uses current time if none specified).1074 1075        :arg time:1076            Can be ``None``, a :class:`!datetime`,1077            or class:`!float` / :class:`!int` unix epoch timestamp.1078            If ``None`` (the default), uses current system time.1079            Naive datetimes are treated as UTC.1080 1081        :returns:1082 1083            A :class:`TotpToken` instance, which can be treated1084            as a sequence of ``(token, expire_time)`` -- see that class1085            for more details.1086 1087        Usage example::1088 1089            >>> # generate a new token, wrapped in a TotpToken instance...1090            >>> otp = TOTP('s3jdvb7qd2r7jpxx')1091            >>> otp.generate(1419622739)1092            <TotpToken token='897212' expire_time=1419622740>1093 1094            >>> # when you just need the token...1095            >>> otp.generate(1419622739).token1096            '897212'1097        """1098        time = self.normalize_time(time)1099        counter = self._time_to_counter(time)1100        if counter < 0:1101            raise ValueError("timestamp must be >= 0")1102        token = self._generate(counter)1103        return TotpToken(self, token, counter)1104 1105    def _generate(self, counter):1106        """1107        base implementation of HOTP token generation algorithm.1108 1109        :arg counter: HOTP counter, as non-negative integer1110        :returns: token as unicode string1111        """1112        # generate digest1113        assert isinstance(counter, int_types), "counter must be integer"1114        assert counter >= 0, "counter must be non-negative"1115        keyed_hmac = self._keyed_hmac1116        if keyed_hmac is None:1117            keyed_hmac = self._keyed_hmac = compile_hmac(self.alg, self.key)1118        digest = keyed_hmac(_pack_uint64(counter))1119        digest_size = keyed_hmac.digest_info.digest_size1120        assert len(digest) == digest_size, "digest_size: sanity check failed"1121 1122        # derive 31-bit token value1123        assert digest_size >= 20, "digest_size: sanity check 2 failed" # otherwise 0xF+4 will run off end of hash.1124        offset = byte_elem_value(digest[-1]) & 0xF1125        value = _unpack_uint32(digest[offset:offset+4])[0] & 0x7fffffff1126 1127        # render to decimal string, return last <digits> chars1128        # NOTE: the 10'th digit is not as secure, as it can only take on values 0-2, not 0-9,1129        #       due to 31-bit mask on int ">I". But some servers / clients use it :|1130        #       if 31-bit mask removed (which breaks spec), would only get values 0-4.1131        digits = self.digits1132        assert 0 < digits < 11, "digits: sanity check failed"1133        return (u("%0*d") % (digits, value))[-digits:]1134 1135    #=============================================================================1136    # token verification1137    #=============================================================================1138 1139    @classmethod1140    def verify(cls, token, source, **kwds):1141        r"""1142        Convenience wrapper around :meth:`TOTP.from_source` and :meth:`TOTP.match`.1143 1144        This parses a TOTP key & configuration from the specified source,1145        and tries and match the token.1146        It's designed to parallel the :meth:`passlib.ifc.PasswordHash.verify` method.1147 1148        :param token:1149            Token string to match.1150 1151        :param source:1152            Serialized TOTP key.1153            Can be anything accepted by :meth:`TOTP.from_source`.1154 1155        :param \\*\\*kwds:1156            All additional keywords passed to :meth:`TOTP.match`.1157 1158        :return:1159            A :class:`TotpMatch` instance, or raises a :exc:`TokenError`.1160        """1161        return cls.from_source(source).match(token, **kwds)1162 1163    def match(self, token, time=None, window=30, skew=0, last_counter=None):1164        """1165        Match TOTP token against specified timestamp.1166        Searches within a window before & after the provided time,1167        in order to account for transmission delay and small amounts of skew in the client's clock.1168 1169        :arg token:1170            Token to validate.1171            may be integer or string (whitespace and hyphens are ignored).1172 1173        :param time:1174            Unix epoch timestamp, can be any of :class:`!float`, :class:`!int`, or :class:`!datetime`.1175            if ``None`` (the default), uses current system time.1176            *this should correspond to the time the token was received from the client*.1177 1178        :param int window:1179            How far backward and forward in time to search for a match.1180            Measured in seconds. Defaults to ``30``.  Typically only useful if set1181            to multiples of :attr:`period`.1182 1183        :param int skew:1184            Adjust timestamp by specified value, to account for excessive1185            client clock skew. Measured in seconds. Defaults to ``0``.1186 1187            Negative skew (the common case) indicates transmission delay,1188            and/or that the client clock is running behind the server.1189 1190            Positive skew indicates the client clock is running ahead of the server1191            (and by enough that it cancels out any negative skew added by1192            the transmission delay).1193 1194            You should ensure the server clock uses a reliable time source such as NTP,1195            so that only the client clock's inaccuracy needs to be accounted for.1196 1197            This is an advanced parameter that should usually be left at ``0``;1198            The **window** parameter is usually enough to account1199            for any observed transmission delay.1200 

Showing the first 1,200 of 1909 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai