/usr/lib/python3/dist-packages/passlib/pwd.py is in python3-passlib 1.7.1-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 | """passlib.pwd -- password generation helpers"""
#=============================================================================
# imports
#=============================================================================
from __future__ import absolute_import, division, print_function, unicode_literals
# core
import codecs
from collections import defaultdict, MutableMapping
from math import ceil, log as logf
import logging; log = logging.getLogger(__name__)
import pkg_resources
import os
# site
# pkg
from passlib import exc
from passlib.utils.compat import PY2, irange, itervalues, int_types
from passlib.utils import rng, getrandstr, to_unicode
from passlib.utils.decor import memoized_property
# local
__all__ = [
"genword", "default_charsets",
"genphrase", "default_wordsets",
]
#=============================================================================
# constants
#=============================================================================
# XXX: rename / publically document this map?
entropy_aliases = dict(
# barest protection from throttled online attack
unsafe=12,
# some protection from unthrottled online attack
weak=24,
# some protection from offline attacks
fair=36,
# reasonable protection from offline attacks
strong=48,
# very good protection from offline attacks
secure=60,
)
#=============================================================================
# internal helpers
#=============================================================================
def _superclasses(obj, cls):
"""return remaining classes in object's MRO after cls"""
mro = type(obj).__mro__
return mro[mro.index(cls)+1:]
def _self_info_rate(source):
"""
returns 'rate of self-information' --
i.e. average (per-symbol) entropy of the sequence **source**,
where probability of a given symbol occurring is calculated based on
the number of occurrences within the sequence itself.
if all elements of the source are unique, this should equal ``log(len(source), 2)``.
:arg source:
iterable containing 0+ symbols
(e.g. list of strings or ints, string of characters, etc).
:returns:
float bits of entropy
"""
try:
size = len(source)
except TypeError:
# if len() doesn't work, calculate size by summing counts later
size = None
counts = defaultdict(int)
for char in source:
counts[char] += 1
if size is None:
values = counts.values()
size = sum(values)
else:
values = itervalues(counts)
if not size:
return 0
# NOTE: the following performs ``- sum(value / size * logf(value / size, 2) for value in values)``,
# it just does so with as much pulled out of the sum() loop as possible...
return logf(size, 2) - sum(value * logf(value, 2) for value in values) / size
# def _total_self_info(source):
# """
# return total self-entropy of a sequence
# (the average entropy per symbol * size of sequence)
# """
# return _self_info_rate(source) * len(source)
def _open_asset_path(path, encoding=None):
"""
:param asset_path:
string containing absolute path to file,
or package-relative path using format
``"python.module:relative/file/path"``.
:returns:
filehandle opened in 'rb' mode
(unless encoding explicitly specified)
"""
if encoding:
return codecs.getreader(encoding)(_open_asset_path(path))
if os.path.isabs(path):
return open(path, "rb")
package, sep, subpath = path.partition(":")
if not sep:
raise ValueError("asset path must be absolute file path "
"or use 'pkg.name:sub/path' format: %r" % (path,))
return pkg_resources.resource_stream(package, subpath)
#: type aliases
_sequence_types = (list, tuple)
_set_types = (set, frozenset)
#: set of elements that ensure_unique() has validated already.
_ensure_unique_cache = set()
def _ensure_unique(source, param="source"):
"""
helper for generators --
Throws ValueError if source elements aren't unique.
Error message will display (abbreviated) repr of the duplicates in a string/list
"""
# check cache to speed things up for frozensets / tuples / strings
cache = _ensure_unique_cache
hashable = True
try:
if source in cache:
return True
except TypeError:
hashable = False
# check if it has dup elements
if isinstance(source, _set_types) or len(set(source)) == len(source):
if hashable:
try:
cache.add(source)
except TypeError:
# XXX: under pypy, "list() in set()" above doesn't throw TypeError,
# but trying to add unhashable it to a set *does*.
pass
return True
# build list of duplicate values
seen = set()
dups = set()
for elem in source:
(dups if elem in seen else seen).add(elem)
dups = sorted(dups)
trunc = 8
if len(dups) > trunc:
trunc = 5
dup_repr = ", ".join(repr(str(word)) for word in dups[:trunc])
if len(dups) > trunc:
dup_repr += ", ... plus %d others" % (len(dups) - trunc)
# throw error
raise ValueError("`%s` cannot contain duplicate elements: %s" %
(param, dup_repr))
#=============================================================================
# base generator class
#=============================================================================
class SequenceGenerator(object):
"""
Base class used by word & phrase generators.
These objects take a series of options, corresponding
to those of the :func:`generate` function.
They act as callables which can be used to generate a password
or a list of 1+ passwords. They also expose some read-only
informational attributes.
Parameters
----------
:param entropy:
Optionally specify the amount of entropy the resulting passwords
should contain (as measured with respect to the generator itself).
This will be used to auto-calculate the required password size.
:param length:
Optionally specify the length of password to generate,
measured as count of whatever symbols the subclass uses (characters or words).
Note if ``entropy`` requires a larger minimum length,
that will be used instead.
:param rng:
Optionally provide a custom RNG source to use.
Should be an instance of :class:`random.Random`,
defaults to :class:`random.SystemRandom`.
Attributes
----------
.. autoattribute:: length
.. autoattribute:: symbol_count
.. autoattribute:: entropy_per_symbol
.. autoattribute:: entropy
Subclassing
-----------
Subclasses must implement the ``.__next__()`` method,
and set ``.symbol_count`` before calling base ``__init__`` method.
"""
#=============================================================================
# instance attrs
#=============================================================================
#: requested size of final password
length = None
#: requested entropy of final password
requested_entropy = "strong"
#: random number source to use
rng = rng
#: number of potential symbols (must be filled in by subclass)
symbol_count = None
#=============================================================================
# init
#=============================================================================
def __init__(self, entropy=None, length=None, rng=None, **kwds):
# make sure subclass set things up correctly
assert self.symbol_count is not None, "subclass must set .symbol_count"
# init length & requested entropy
if entropy is not None or length is None:
if entropy is None:
entropy = self.requested_entropy
entropy = entropy_aliases.get(entropy, entropy)
if entropy <= 0:
raise ValueError("`entropy` must be positive number")
min_length = int(ceil(entropy / self.entropy_per_symbol))
if length is None or length < min_length:
length = min_length
self.requested_entropy = entropy
if length < 1:
raise ValueError("`length` must be positive integer")
self.length = length
# init other common options
if rng is not None:
self.rng = rng
# hand off to parent
if kwds and _superclasses(self, SequenceGenerator) == (object,):
raise TypeError("Unexpected keyword(s): %s" % ", ".join(kwds.keys()))
super(SequenceGenerator, self).__init__(**kwds)
#=============================================================================
# informational helpers
#=============================================================================
@memoized_property
def entropy_per_symbol(self):
"""
Average entropy per symbol (assuming all symbols have equal probability)
"""
return logf(self.symbol_count, 2)
@memoized_property
def entropy(self):
"""
Effective entropy of generated passwords.
This value will always be a multiple of :attr:`entropy_per_symbol`.
If entropy is specified in constructor, :attr:`length` will be chosen so
so that this value is the smallest multiple >= :attr:`requested_entropy`.
"""
return self.length * self.entropy_per_symbol
#=============================================================================
# generation
#=============================================================================
def __next__(self):
"""main generation function, should create one password/phrase"""
raise NotImplementedError("implement in subclass")
def __call__(self, returns=None):
"""
frontend used by genword() / genphrase() to create passwords
"""
if returns is None:
return next(self)
elif isinstance(returns, int_types):
return [next(self) for _ in irange(returns)]
elif returns is iter:
return self
else:
raise exc.ExpectedTypeError(returns, "<None>, int, or <iter>", "returns")
def __iter__(self):
return self
if PY2:
def next(self):
return self.__next__()
#=============================================================================
# eoc
#=============================================================================
#=============================================================================
# default charsets
#=============================================================================
#: global dict of predefined characters sets
default_charsets = dict(
# ascii letters, digits, and some punctuation
ascii_72='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*?/',
# ascii letters and digits
ascii_62='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
# ascii_50, without visually similar '1IiLl', '0Oo', '5S', '8B'
ascii_50='234679abcdefghjkmnpqrstuvwxyzACDEFGHJKMNPQRTUVWXYZ',
# lower case hexadecimal
hex='0123456789abcdef',
)
#=============================================================================
# password generator
#=============================================================================
class WordGenerator(SequenceGenerator):
"""
Class which generates passwords by randomly choosing from a string of unique characters.
Parameters
----------
:param chars:
custom character string to draw from.
:param charset:
predefined charset to draw from.
:param \*\*kwds:
all other keywords passed to the :class:`SequenceGenerator` parent class.
Attributes
----------
.. autoattribute:: chars
.. autoattribute:: charset
.. autoattribute:: default_charsets
"""
#=============================================================================
# instance attrs
#=============================================================================
#: Predefined character set in use (set to None for instances using custom 'chars')
charset = "ascii_62"
#: string of chars to draw from -- usually filled in from charset
chars = None
#=============================================================================
# init
#=============================================================================
def __init__(self, chars=None, charset=None, **kwds):
# init chars and charset
if chars:
if charset:
raise TypeError("`chars` and `charset` are mutually exclusive")
else:
if not charset:
charset = self.charset
assert charset
chars = default_charsets[charset]
self.charset = charset
chars = to_unicode(chars, param="chars")
_ensure_unique(chars, param="chars")
self.chars = chars
# hand off to parent
super(WordGenerator, self).__init__(**kwds)
# log.debug("WordGenerator(): entropy/char=%r", self.entropy_per_symbol)
#=============================================================================
# informational helpers
#=============================================================================
@memoized_property
def symbol_count(self):
return len(self.chars)
#=============================================================================
# generation
#=============================================================================
def __next__(self):
# XXX: could do things like optionally ensure certain character groups
# (e.g. letters & punctuation) are included
return getrandstr(self.rng, self.chars, self.length)
#=============================================================================
# eoc
#=============================================================================
def genword(entropy=None, length=None, returns=None, **kwds):
"""Generate one or more random passwords.
This function uses :mod:`random.SystemRandom` to generate
one or more passwords using various character sets.
The complexity of the password can be specified
by size, or by the desired amount of entropy.
Usage Example::
>>> # generate a random alphanumeric string with 48 bits of entropy (the default)
>>> from passlib import pwd
>>> pwd.genword()
'DnBHvDjMK6'
>>> # generate a random hexadecimal string with 52 bits of entropy
>>> pwd.genword(entropy=52, charset="hex")
'310f1a7ac793f'
:param entropy:
Strength of resulting password, measured in 'guessing entropy' bits.
An appropriate **length** value will be calculated
based on the requested entropy amount, and the size of the character set.
This can be a positive integer, or one of the following preset
strings: ``"weak"`` (24), ``"fair"`` (36),
``"strong"`` (48), and ``"secure"`` (56).
If neither this or **length** is specified, **entropy** will default
to ``"strong"`` (48).
:param length:
Size of resulting password, measured in characters.
If omitted, the size is auto-calculated based on the **entropy** parameter.
If both **entropy** and **length** are specified,
the stronger value will be used.
:param returns:
Controls what this function returns:
* If ``None`` (the default), this function will generate a single password.
* If an integer, this function will return a list containing that many passwords.
* If the ``iter`` constant, will return an iterator that yields passwords.
:param chars:
Optionally specify custom string of characters to use when randomly
generating a password. This option cannot be combined with **charset**.
:param charset:
The predefined character set to draw from (if not specified by **chars**).
There are currently four presets available:
* ``"ascii_62"`` (the default) -- all digits and ascii upper & lowercase letters.
Provides ~5.95 entropy per character.
* ``"ascii_50"`` -- subset which excludes visually similar characters
(``1IiLl0Oo5S8B``). Provides ~5.64 entropy per character.
* ``"ascii_72"`` -- all digits and ascii upper & lowercase letters,
as well as some punctuation. Provides ~6.17 entropy per character.
* ``"hex"`` -- Lower case hexadecimal. Providers 4 bits of entropy per character.
:returns:
:class:`!unicode` string containing randomly generated password;
or list of 1+ passwords if :samp:`returns={int}` is specified.
"""
gen = WordGenerator(length=length, entropy=entropy, **kwds)
return gen(returns)
#=============================================================================
# default wordsets
#=============================================================================
def _load_wordset(asset_path):
"""
load wordset from compressed datafile within package data.
file should be utf-8 encoded
:param asset_path:
string containing absolute path to wordset file,
or "python.module:relative/file/path".
:returns:
tuple of words, as loaded from specified words file.
"""
# open resource file, convert to tuple of words (strip blank lines & ws)
with _open_asset_path(asset_path, "utf-8") as fh:
gen = (word.strip() for word in fh)
words = tuple(word for word in gen if word)
# NOTE: works but not used
# # detect if file uses "<int> <word>" format, and strip numeric prefix
# def extract(row):
# idx, word = row.replace("\t", " ").split(" ", 1)
# if not idx.isdigit():
# raise ValueError("row is not dice index + word")
# return word
# try:
# extract(words[-1])
# except ValueError:
# pass
# else:
# words = tuple(extract(word) for word in words)
log.debug("loaded %d-element wordset from %r", len(words), asset_path)
return words
class WordsetDict(MutableMapping):
"""
Special mapping used to store dictionary of wordsets.
Different from a regular dict in that some wordsets
may be lazy-loaded from an asset path.
"""
#: dict of key -> asset path
paths = None
#: dict of key -> value
_loaded = None
def __init__(self, *args, **kwds):
self.paths = {}
self._loaded = {}
super(WordsetDict, self).__init__(*args, **kwds)
def __getitem__(self, key):
try:
return self._loaded[key]
except KeyError:
pass
path = self.paths[key]
value = self._loaded[key] = _load_wordset(path)
return value
def set_path(self, key, path):
"""
set asset path to lazy-load wordset from.
"""
self.paths[key] = path
def __setitem__(self, key, value):
self._loaded[key] = value
def __delitem__(self, key):
if key in self:
del self._loaded[key]
self.paths.pop(key, None)
else:
del self.paths[key]
@property
def _keyset(self):
keys = set(self._loaded)
keys.update(self.paths)
return keys
def __iter__(self):
return iter(self._keyset)
def __len__(self):
return len(self._keyset)
# NOTE: speeds things up, and prevents contains from lazy-loading
def __contains__(self, key):
return key in self._loaded or key in self.paths
#: dict of predefined word sets.
#: key is name of wordset, value should be sequence of words.
default_wordsets = WordsetDict()
# register the wordsets built into passlib
for name in "eff_long eff_short eff_prefixed bip39".split():
default_wordsets.set_path(name, "passlib:_data/wordsets/%s.txt" % name)
#=============================================================================
# passphrase generator
#=============================================================================
class PhraseGenerator(SequenceGenerator):
"""class which generates passphrases by randomly choosing
from a list of unique words.
:param wordset:
wordset to draw from.
:param preset:
name of preset wordlist to use instead of ``wordset``.
:param spaces:
whether to insert spaces between words in output (defaults to ``True``).
:param \*\*kwds:
all other keywords passed to the :class:`SequenceGenerator` parent class.
.. autoattribute:: wordset
"""
#=============================================================================
# instance attrs
#=============================================================================
#: predefined wordset to use
wordset = "eff_long"
#: list of words to draw from
words = None
#: separator to use when joining words
sep = " "
#=============================================================================
# init
#=============================================================================
def __init__(self, wordset=None, words=None, sep=None, **kwds):
# load wordset
if words is not None:
if wordset is not None:
raise TypeError("`words` and `wordset` are mutually exclusive")
else:
if wordset is None:
wordset = self.wordset
assert wordset
words = default_wordsets[wordset]
self.wordset = wordset
# init words
if not isinstance(words, _sequence_types):
words = tuple(words)
_ensure_unique(words, param="words")
self.words = words
# init separator
if sep is None:
sep = self.sep
sep = to_unicode(sep, param="sep")
self.sep = sep
# hand off to parent
super(PhraseGenerator, self).__init__(**kwds)
##log.debug("PhraseGenerator(): entropy/word=%r entropy/char=%r min_chars=%r",
## self.entropy_per_symbol, self.entropy_per_char, self.min_chars)
#=============================================================================
# informational helpers
#=============================================================================
@memoized_property
def symbol_count(self):
return len(self.words)
#=============================================================================
# generation
#=============================================================================
def __next__(self):
words = (self.rng.choice(self.words) for _ in irange(self.length))
return self.sep.join(words)
#=============================================================================
# eoc
#=============================================================================
def genphrase(entropy=None, length=None, returns=None, **kwds):
"""Generate one or more random password / passphrases.
This function uses :mod:`random.SystemRandom` to generate
one or more passwords; it can be configured to generate
alphanumeric passwords, or full english phrases.
The complexity of the password can be specified
by size, or by the desired amount of entropy.
Usage Example::
>>> # generate random phrase with 48 bits of entropy
>>> from passlib import pwd
>>> pwd.genphrase()
'gangly robbing salt shove'
>>> # generate a random phrase with 52 bits of entropy
>>> # using a particular wordset
>>> pwd.genword(entropy=52, wordset="bip39")
'wheat dilemma reward rescue diary'
:param entropy:
Strength of resulting password, measured in 'guessing entropy' bits.
An appropriate **length** value will be calculated
based on the requested entropy amount, and the size of the word set.
This can be a positive integer, or one of the following preset
strings: ``"weak"`` (24), ``"fair"`` (36),
``"strong"`` (48), and ``"secure"`` (56).
If neither this or **length** is specified, **entropy** will default
to ``"strong"`` (48).
:param length:
Length of resulting password, measured in words.
If omitted, the size is auto-calculated based on the **entropy** parameter.
If both **entropy** and **length** are specified,
the stronger value will be used.
:param returns:
Controls what this function returns:
* If ``None`` (the default), this function will generate a single password.
* If an integer, this function will return a list containing that many passwords.
* If the ``iter`` builtin, will return an iterator that yields passwords.
:param words:
Optionally specifies a list/set of words to use when randomly generating a passphrase.
This option cannot be combined with **wordset**.
:param wordset:
The predefined word set to draw from (if not specified by **words**).
There are currently four presets available:
``"eff_long"`` (the default)
Wordset containing 7776 english words of ~7 letters.
Constructed by the EFF, it offers ~12.9 bits of entropy per word.
This wordset (and the other ``"eff_"`` wordsets)
were `created by the EFF <https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases>`_
to aid in generating passwords. See their announcement page
for more details about the design & properties of these wordsets.
``"eff_short"``
Wordset containing 1296 english words of ~4.5 letters.
Constructed by the EFF, it offers ~10.3 bits of entropy per word.
``"eff_prefixed"``
Wordset containing 1296 english words of ~8 letters,
selected so that they each have a unique 3-character prefix.
Constructed by the EFF, it offers ~10.3 bits of entropy per word.
``"bip39"``
Wordset of 2048 english words of ~5 letters,
selected so that they each have a unique 4-character prefix.
Published as part of Bitcoin's `BIP 39 <https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt>`_,
this wordset has exactly 11 bits of entropy per word.
This list offers words that are typically shorter than ``"eff_long"``
(at the cost of slightly less entropy); and much shorter than
``"eff_prefixed"`` (at the cost of a longer unique prefix).
:param sep:
Optional separator to use when joining words.
Defaults to ``" "`` (a space), but can be an empty string, a hyphen, etc.
:returns:
:class:`!unicode` string containing randomly generated passphrase;
or list of 1+ passphrases if :samp:`returns={int}` is specified.
"""
gen = PhraseGenerator(entropy=entropy, length=length, **kwds)
return gen(returns)
#=============================================================================
# strength measurement
#
# NOTE:
# for a little while, had rough draft of password strength measurement alg here.
# but not sure if there's value in yet another measurement algorithm,
# that's not just duplicating the effort of libraries like zxcbn.
# may revive it later, but for now, leaving some refs to others out there:
# * NIST 800-63 has simple alg
# * zxcvbn (https://tech.dropbox.com/2012/04/zxcvbn-realistic-password-strength-estimation/)
# might also be good, and has approach similar to composite approach i was already thinking about,
# but much more well thought out.
# * passfault (https://github.com/c-a-m/passfault) looks thorough,
# but may have licensing issues, plus porting to python looks like very big job :(
# * give a look at running things through zlib - might be able to cheaply
# catch extra redundancies.
#=============================================================================
#=============================================================================
# eof
#=============================================================================
|