Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
pwd.py810 linesDownload Raw Back to passlib
1"""passlib.pwd -- password generation helpers"""2#=============================================================================3# imports4#=============================================================================5from __future__ import absolute_import, division, print_function, unicode_literals6# core7import codecs8from collections import defaultdict9try:10    from collections.abc import MutableMapping11except ImportError:12    # py2 compat13    from collections import MutableMapping14from math import ceil, log as logf15import logging; log = logging.getLogger(__name__)16import pkg_resources17import os18# site19# pkg20from passlib import exc21from passlib.utils.compat import PY2, irange, itervalues, int_types22from passlib.utils import rng, getrandstr, to_unicode23from passlib.utils.decor import memoized_property24# local25__all__ = [26    "genword", "default_charsets",27    "genphrase", "default_wordsets",28]29 30#=============================================================================31# constants32#=============================================================================33 34# XXX: rename / publically document this map?35entropy_aliases = dict(36    # barest protection from throttled online attack37    unsafe=12,38 39    # some protection from unthrottled online attack40    weak=24,41 42    # some protection from offline attacks43    fair=36,44 45    # reasonable protection from offline attacks46    strong=48,47 48    # very good protection from offline attacks49    secure=60,50)51 52#=============================================================================53# internal helpers54#=============================================================================55 56def _superclasses(obj, cls):57    """return remaining classes in object's MRO after cls"""58    mro = type(obj).__mro__59    return mro[mro.index(cls)+1:]60 61 62def _self_info_rate(source):63    """64    returns 'rate of self-information' --65    i.e. average (per-symbol) entropy of the sequence **source**,66    where probability of a given symbol occurring is calculated based on67    the number of occurrences within the sequence itself.68 69    if all elements of the source are unique, this should equal ``log(len(source), 2)``.70 71    :arg source:72        iterable containing 0+ symbols73        (e.g. list of strings or ints, string of characters, etc).74 75    :returns:76        float bits of entropy77    """78    try:79        size = len(source)80    except TypeError:81        # if len() doesn't work, calculate size by summing counts later82        size = None83    counts = defaultdict(int)84    for char in source:85        counts[char] += 186    if size is None:87        values = counts.values()88        size = sum(values)89    else:90        values = itervalues(counts)91    if not size:92        return 093    # NOTE: the following performs ``- sum(value / size * logf(value / size, 2) for value in values)``,94    #       it just does so with as much pulled out of the sum() loop as possible...95    return logf(size, 2) - sum(value * logf(value, 2) for value in values) / size96 97 98# def _total_self_info(source):99#     """100#     return total self-entropy of a sequence101#     (the average entropy per symbol * size of sequence)102#     """103#     return _self_info_rate(source) * len(source)104 105 106def _open_asset_path(path, encoding=None):107    """108    :param asset_path:109        string containing absolute path to file,110        or package-relative path using format111        ``"python.module:relative/file/path"``.112 113    :returns:114        filehandle opened in 'rb' mode115        (unless encoding explicitly specified)116    """117    if encoding:118        return codecs.getreader(encoding)(_open_asset_path(path))119    if os.path.isabs(path):120        return open(path, "rb")121    package, sep, subpath = path.partition(":")122    if not sep:123        raise ValueError("asset path must be absolute file path "124                         "or use 'pkg.name:sub/path' format: %r" % (path,))125    return pkg_resources.resource_stream(package, subpath)126 127 128#: type aliases129_sequence_types = (list, tuple)130_set_types = (set, frozenset)131 132#: set of elements that ensure_unique() has validated already.133_ensure_unique_cache = set()134 135 136def _ensure_unique(source, param="source"):137    """138    helper for generators --139    Throws ValueError if source elements aren't unique.140    Error message will display (abbreviated) repr of the duplicates in a string/list141    """142    # check cache to speed things up for frozensets / tuples / strings143    cache = _ensure_unique_cache144    hashable = True145    try:146        if source in cache:147            return True148    except TypeError:149        hashable = False150 151    # check if it has dup elements152    if isinstance(source, _set_types) or len(set(source)) == len(source):153        if hashable:154            try:155                cache.add(source)156            except TypeError:157                # XXX: under pypy, "list() in set()" above doesn't throw TypeError,158                #      but trying to add unhashable it to a set *does*.159                pass160        return True161 162    # build list of duplicate values163    seen = set()164    dups = set()165    for elem in source:166        (dups if elem in seen else seen).add(elem)167    dups = sorted(dups)168    trunc = 8169    if len(dups) > trunc:170        trunc = 5171    dup_repr = ", ".join(repr(str(word)) for word in dups[:trunc])172    if len(dups) > trunc:173        dup_repr += ", ... plus %d others" % (len(dups) - trunc)174 175    # throw error176    raise ValueError("`%s` cannot contain duplicate elements: %s" %177                     (param, dup_repr))178 179#=============================================================================180# base generator class181#=============================================================================182class SequenceGenerator(object):183    """184    Base class used by word & phrase generators.185 186    These objects take a series of options, corresponding187    to those of the :func:`generate` function.188    They act as callables which can be used to generate a password189    or a list of 1+ passwords. They also expose some read-only190    informational attributes.191 192    Parameters193    ----------194    :param entropy:195        Optionally specify the amount of entropy the resulting passwords196        should contain (as measured with respect to the generator itself).197        This will be used to auto-calculate the required password size.198 199    :param length:200        Optionally specify the length of password to generate,201        measured as count of whatever symbols the subclass uses (characters or words).202        Note if ``entropy`` requires a larger minimum length,203        that will be used instead.204 205    :param rng:206        Optionally provide a custom RNG source to use.207        Should be an instance of :class:`random.Random`,208        defaults to :class:`random.SystemRandom`.209 210    Attributes211    ----------212    .. autoattribute:: length213    .. autoattribute:: symbol_count214    .. autoattribute:: entropy_per_symbol215    .. autoattribute:: entropy216 217    Subclassing218    -----------219    Subclasses must implement the ``.__next__()`` method,220    and set ``.symbol_count`` before calling base ``__init__`` method.221    """222    #=============================================================================223    # instance attrs224    #=============================================================================225 226    #: requested size of final password227    length = None228 229    #: requested entropy of final password230    requested_entropy = "strong"231 232    #: random number source to use233    rng = rng234 235    #: number of potential symbols (must be filled in by subclass)236    symbol_count = None237 238    #=============================================================================239    # init240    #=============================================================================241    def __init__(self, entropy=None, length=None, rng=None, **kwds):242 243        # make sure subclass set things up correctly244        assert self.symbol_count is not None, "subclass must set .symbol_count"245 246        # init length & requested entropy247        if entropy is not None or length is None:248            if entropy is None:249                entropy = self.requested_entropy250            entropy = entropy_aliases.get(entropy, entropy)251            if entropy <= 0:252                raise ValueError("`entropy` must be positive number")253            min_length = int(ceil(entropy / self.entropy_per_symbol))254            if length is None or length < min_length:255                length = min_length256 257        self.requested_entropy = entropy258 259        if length < 1:260            raise ValueError("`length` must be positive integer")261        self.length = length262 263        # init other common options264        if rng is not None:265            self.rng = rng266 267        # hand off to parent268        if kwds and _superclasses(self, SequenceGenerator) == (object,):269            raise TypeError("Unexpected keyword(s): %s" % ", ".join(kwds.keys()))270        super(SequenceGenerator, self).__init__(**kwds)271 272    #=============================================================================273    # informational helpers274    #=============================================================================275 276    @memoized_property277    def entropy_per_symbol(self):278        """279        Average entropy per symbol (assuming all symbols have equal probability)280        """281        return logf(self.symbol_count, 2)282 283    @memoized_property284    def entropy(self):285        """286        Effective entropy of generated passwords.287 288        This value will always be a multiple of :attr:`entropy_per_symbol`.289        If entropy is specified in constructor, :attr:`length` will be chosen so290        so that this value is the smallest multiple >= :attr:`requested_entropy`.291        """292        return self.length * self.entropy_per_symbol293 294    #=============================================================================295    # generation296    #=============================================================================297    def __next__(self):298        """main generation function, should create one password/phrase"""299        raise NotImplementedError("implement in subclass")300 301    def __call__(self, returns=None):302        """303        frontend used by genword() / genphrase() to create passwords304        """305        if returns is None:306            return next(self)307        elif isinstance(returns, int_types):308            return [next(self) for _ in irange(returns)]309        elif returns is iter:310            return self311        else:312            raise exc.ExpectedTypeError(returns, "<None>, int, or <iter>", "returns")313 314    def __iter__(self):315        return self316 317    if PY2:318        def next(self):319            return self.__next__()320 321    #=============================================================================322    # eoc323    #=============================================================================324 325#=============================================================================326# default charsets327#=============================================================================328 329#: global dict of predefined characters sets330default_charsets = dict(331    # ascii letters, digits, and some punctuation332    ascii_72='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*?/',333 334    # ascii letters and digits335    ascii_62='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',336 337    # ascii_50, without visually similar '1IiLl', '0Oo', '5S', '8B'338    ascii_50='234679abcdefghjkmnpqrstuvwxyzACDEFGHJKMNPQRTUVWXYZ',339 340    # lower case hexadecimal341    hex='0123456789abcdef',342)343 344#=============================================================================345# password generator346#=============================================================================347 348class WordGenerator(SequenceGenerator):349    """350    Class which generates passwords by randomly choosing from a string of unique characters.351 352    Parameters353    ----------354    :param chars:355        custom character string to draw from.356 357    :param charset:358        predefined charset to draw from.359 360    :param \\*\\*kwds:361        all other keywords passed to the :class:`SequenceGenerator` parent class.362 363    Attributes364    ----------365    .. autoattribute:: chars366    .. autoattribute:: charset367    .. autoattribute:: default_charsets368    """369    #=============================================================================370    # instance attrs371    #=============================================================================372 373    #: Predefined character set in use (set to None for instances using custom 'chars')374    charset = "ascii_62"375 376    #: string of chars to draw from -- usually filled in from charset377    chars = None378 379    #=============================================================================380    # init381    #=============================================================================382    def __init__(self, chars=None, charset=None, **kwds):383 384        # init chars and charset385        if chars:386            if charset:387                raise TypeError("`chars` and `charset` are mutually exclusive")388        else:389            if not charset:390                charset = self.charset391                assert charset392            chars = default_charsets[charset]393        self.charset = charset394        chars = to_unicode(chars, param="chars")395        _ensure_unique(chars, param="chars")396        self.chars = chars397 398        # hand off to parent399        super(WordGenerator, self).__init__(**kwds)400        # log.debug("WordGenerator(): entropy/char=%r", self.entropy_per_symbol)401 402    #=============================================================================403    # informational helpers404    #=============================================================================405 406    @memoized_property407    def symbol_count(self):408        return len(self.chars)409 410    #=============================================================================411    # generation412    #=============================================================================413 414    def __next__(self):415        # XXX: could do things like optionally ensure certain character groups416        #      (e.g. letters & punctuation) are included417        return getrandstr(self.rng, self.chars, self.length)418 419    #=============================================================================420    # eoc421    #=============================================================================422 423 424def genword(entropy=None, length=None, returns=None, **kwds):425    """Generate one or more random passwords.426 427    This function uses :mod:`random.SystemRandom` to generate428    one or more passwords using various character sets.429    The complexity of the password can be specified430    by size, or by the desired amount of entropy.431 432    Usage Example::433 434        >>> # generate a random alphanumeric string with 48 bits of entropy (the default)435        >>> from passlib import pwd436        >>> pwd.genword()437        'DnBHvDjMK6'438 439        >>> # generate a random hexadecimal string with 52 bits of entropy440        >>> pwd.genword(entropy=52, charset="hex")441        '310f1a7ac793f'442 443    :param entropy:444        Strength of resulting password, measured in 'guessing entropy' bits.445        An appropriate **length** value will be calculated446        based on the requested entropy amount, and the size of the character set.447 448        This can be a positive integer, or one of the following preset449        strings: ``"weak"`` (24), ``"fair"`` (36),450        ``"strong"`` (48), and ``"secure"`` (56).451 452        If neither this or **length** is specified, **entropy** will default453        to ``"strong"`` (48).454 455    :param length:456        Size of resulting password, measured in characters.457        If omitted, the size is auto-calculated based on the **entropy** parameter.458 459        If both **entropy** and **length** are specified,460        the stronger value will be used.461 462    :param returns:463        Controls what this function returns:464 465        * If ``None`` (the default), this function will generate a single password.466        * If an integer, this function will return a list containing that many passwords.467        * If the ``iter`` constant, will return an iterator that yields passwords.468 469    :param chars:470 471        Optionally specify custom string of characters to use when randomly472        generating a password. This option cannot be combined with **charset**.473 474    :param charset:475 476        The predefined character set to draw from (if not specified by **chars**).477        There are currently four presets available:478 479        * ``"ascii_62"`` (the default) -- all digits and ascii upper & lowercase letters.480          Provides ~5.95 entropy per character.481 482        * ``"ascii_50"`` -- subset which excludes visually similar characters483          (``1IiLl0Oo5S8B``). Provides ~5.64 entropy per character.484 485        * ``"ascii_72"`` -- all digits and ascii upper & lowercase letters,486          as well as some punctuation. Provides ~6.17 entropy per character.487 488        * ``"hex"`` -- Lower case hexadecimal.  Providers 4 bits of entropy per character.489 490    :returns:491        :class:`!unicode` string containing randomly generated password;492        or list of 1+ passwords if :samp:`returns={int}` is specified.493    """494    gen = WordGenerator(length=length, entropy=entropy, **kwds)495    return gen(returns)496 497#=============================================================================498# default wordsets499#=============================================================================500 501def _load_wordset(asset_path):502    """503    load wordset from compressed datafile within package data.504    file should be utf-8 encoded505 506    :param asset_path:507        string containing  absolute path to wordset file,508        or "python.module:relative/file/path".509 510    :returns:511        tuple of words, as loaded from specified words file.512    """513    # open resource file, convert to tuple of words (strip blank lines & ws)514    with _open_asset_path(asset_path, "utf-8") as fh:515        gen = (word.strip() for word in fh)516        words = tuple(word for word in gen if word)517 518    # NOTE: works but not used519    # # detect if file uses "<int> <word>" format, and strip numeric prefix520    # def extract(row):521    #     idx, word = row.replace("\t", " ").split(" ", 1)522    #     if not idx.isdigit():523    #         raise ValueError("row is not dice index + word")524    #     return word525    # try:526    #     extract(words[-1])527    # except ValueError:528    #     pass529    # else:530    #     words = tuple(extract(word) for word in words)531 532    log.debug("loaded %d-element wordset from %r", len(words), asset_path)533    return words534 535 536class WordsetDict(MutableMapping):537    """538    Special mapping used to store dictionary of wordsets.539    Different from a regular dict in that some wordsets540    may be lazy-loaded from an asset path.541    """542 543    #: dict of key -> asset path544    paths = None545 546    #: dict of key -> value547    _loaded = None548 549    def __init__(self, *args, **kwds):550        self.paths = {}551        self._loaded = {}552        super(WordsetDict, self).__init__(*args, **kwds)553 554    def __getitem__(self, key):555        try:556            return self._loaded[key]557        except KeyError:558            pass559        path = self.paths[key]560        value = self._loaded[key] = _load_wordset(path)561        return value562 563    def set_path(self, key, path):564        """565        set asset path to lazy-load wordset from.566        """567        self.paths[key] = path568 569    def __setitem__(self, key, value):570        self._loaded[key] = value571 572    def __delitem__(self, key):573        if key in self:574            del self._loaded[key]575            self.paths.pop(key, None)576        else:577            del self.paths[key]578 579    @property580    def _keyset(self):581        keys = set(self._loaded)582        keys.update(self.paths)583        return keys584 585    def __iter__(self):586        return iter(self._keyset)587 588    def __len__(self):589        return len(self._keyset)590 591    # NOTE: speeds things up, and prevents contains from lazy-loading592    def __contains__(self, key):593        return key in self._loaded or key in self.paths594 595 596#: dict of predefined word sets.597#: key is name of wordset, value should be sequence of words.598default_wordsets = WordsetDict()599 600# register the wordsets built into passlib601for name in "eff_long eff_short eff_prefixed bip39".split():602    default_wordsets.set_path(name, "passlib:_data/wordsets/%s.txt" % name)603 604#=============================================================================605# passphrase generator606#=============================================================================607class PhraseGenerator(SequenceGenerator):608    """class which generates passphrases by randomly choosing609    from a list of unique words.610 611    :param wordset:612        wordset to draw from.613    :param preset:614        name of preset wordlist to use instead of ``wordset``.615    :param spaces:616        whether to insert spaces between words in output (defaults to ``True``).617    :param \\*\\*kwds:618        all other keywords passed to the :class:`SequenceGenerator` parent class.619 620    .. autoattribute:: wordset621    """622    #=============================================================================623    # instance attrs624    #=============================================================================625 626    #: predefined wordset to use627    wordset = "eff_long"628 629    #: list of words to draw from630    words = None631 632    #: separator to use when joining words633    sep = " "634 635    #=============================================================================636    # init637    #=============================================================================638    def __init__(self, wordset=None, words=None, sep=None, **kwds):639 640        # load wordset641        if words is not None:642            if wordset is not None:643                raise TypeError("`words` and `wordset` are mutually exclusive")644        else:645            if wordset is None:646                wordset = self.wordset647                assert wordset648            words = default_wordsets[wordset]649        self.wordset = wordset650 651        # init words652        if not isinstance(words, _sequence_types):653            words = tuple(words)654        _ensure_unique(words, param="words")655        self.words = words656 657        # init separator658        if sep is None:659            sep = self.sep660        sep = to_unicode(sep, param="sep")661        self.sep = sep662 663        # hand off to parent664        super(PhraseGenerator, self).__init__(**kwds)665        ##log.debug("PhraseGenerator(): entropy/word=%r entropy/char=%r min_chars=%r",666        ##          self.entropy_per_symbol, self.entropy_per_char, self.min_chars)667 668    #=============================================================================669    # informational helpers670    #=============================================================================671 672    @memoized_property673    def symbol_count(self):674        return len(self.words)675 676    #=============================================================================677    # generation678    #=============================================================================679 680    def __next__(self):681        words = (self.rng.choice(self.words) for _ in irange(self.length))682        return self.sep.join(words)683 684    #=============================================================================685    # eoc686    #=============================================================================687 688 689def genphrase(entropy=None, length=None, returns=None, **kwds):690    """Generate one or more random password / passphrases.691 692    This function uses :mod:`random.SystemRandom` to generate693    one or more passwords; it can be configured to generate694    alphanumeric passwords, or full english phrases.695    The complexity of the password can be specified696    by size, or by the desired amount of entropy.697 698    Usage Example::699 700        >>> # generate random phrase with 48 bits of entropy701        >>> from passlib import pwd702        >>> pwd.genphrase()703        'gangly robbing salt shove'704 705        >>> # generate a random phrase with 52 bits of entropy706        >>> # using a particular wordset707        >>> pwd.genword(entropy=52, wordset="bip39")708        'wheat dilemma reward rescue diary'709 710    :param entropy:711        Strength of resulting password, measured in 'guessing entropy' bits.712        An appropriate **length** value will be calculated713        based on the requested entropy amount, and the size of the word set.714 715        This can be a positive integer, or one of the following preset716        strings: ``"weak"`` (24), ``"fair"`` (36),717        ``"strong"`` (48), and ``"secure"`` (56).718 719        If neither this or **length** is specified, **entropy** will default720        to ``"strong"`` (48).721 722    :param length:723        Length of resulting password, measured in words.724        If omitted, the size is auto-calculated based on the **entropy** parameter.725 726        If both **entropy** and **length** are specified,727        the stronger value will be used.728 729    :param returns:730        Controls what this function returns:731 732        * If ``None`` (the default), this function will generate a single password.733        * If an integer, this function will return a list containing that many passwords.734        * If the ``iter`` builtin, will return an iterator that yields passwords.735 736    :param words:737 738        Optionally specifies a list/set of words to use when randomly generating a passphrase.739        This option cannot be combined with **wordset**.740 741    :param wordset:742 743        The predefined word set to draw from (if not specified by **words**).744        There are currently four presets available:745 746        ``"eff_long"`` (the default)747 748            Wordset containing 7776 english words of ~7 letters.749            Constructed by the EFF, it offers ~12.9 bits of entropy per word.750 751            This wordset (and the other ``"eff_"`` wordsets)752            were `created by the EFF <https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases>`_753            to aid in generating passwords.  See their announcement page754            for more details about the design & properties of these wordsets.755 756        ``"eff_short"``757 758            Wordset containing 1296 english words of ~4.5 letters.759            Constructed by the EFF, it offers ~10.3 bits of entropy per word.760 761        ``"eff_prefixed"``762 763            Wordset containing 1296 english words of ~8 letters,764            selected so that they each have a unique 3-character prefix.765            Constructed by the EFF, it offers ~10.3 bits of entropy per word.766 767        ``"bip39"``768 769            Wordset of 2048 english words of ~5 letters,770            selected so that they each have a unique 4-character prefix.771            Published as part of Bitcoin's `BIP 39 <https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt>`_,772            this wordset has exactly 11 bits of entropy per word.773 774            This list offers words that are typically shorter than ``"eff_long"``775            (at the cost of slightly less entropy); and much shorter than776            ``"eff_prefixed"`` (at the cost of a longer unique prefix).777 778    :param sep:779        Optional separator to use when joining words.780        Defaults to ``" "`` (a space), but can be an empty string, a hyphen, etc.781 782    :returns:783        :class:`!unicode` string containing randomly generated passphrase;784        or list of 1+ passphrases if :samp:`returns={int}` is specified.785    """786    gen = PhraseGenerator(entropy=entropy, length=length, **kwds)787    return gen(returns)788 789#=============================================================================790# strength measurement791#792# NOTE:793# for a little while, had rough draft of password strength measurement alg here.794# but not sure if there's value in yet another measurement algorithm,795# that's not just duplicating the effort of libraries like zxcbn.796# may revive it later, but for now, leaving some refs to others out there:797#    * NIST 800-63 has simple alg798#    * zxcvbn (https://tech.dropbox.com/2012/04/zxcvbn-realistic-password-strength-estimation/)799#      might also be good, and has approach similar to composite approach i was already thinking about,800#      but much more well thought out.801#    * passfault (https://github.com/c-a-m/passfault) looks thorough,802#      but may have licensing issues, plus porting to python looks like very big job :(803#    * give a look at running things through zlib - might be able to cheaply804#      catch extra redundancies.805#=============================================================================806 807#=============================================================================808# eof809#=============================================================================810 
codekingpro/portable-devtools · Team Ai