Home | History | Annotate | Download | only in webapp2_extras
      1 # -*- coding: utf-8 -*-
      2 """
      3     webapp2_extras.security
      4     =======================
      5 
      6     Security related helpers such as secure password hashing tools and a
      7     random token generator.
      8 
      9     :copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
     10     :license: BSD, see LICENSE for more details.
     11     :copyright: (c) 2011 Yesudeep Mangalapilly <yesudeep (at] gmail.com>
     12     :license: Apache Sotware License, see LICENSE for details.
     13 """
     14 from __future__ import division
     15 
     16 import hashlib
     17 import hmac
     18 import math
     19 import random
     20 import string
     21 
     22 import webapp2
     23 
     24 _rng = random.SystemRandom()
     25 
     26 HEXADECIMAL_DIGITS = string.digits + 'abcdef'
     27 DIGITS = string.digits
     28 LOWERCASE_ALPHA = string.lowercase
     29 UPPERCASE_ALPHA = string.uppercase
     30 LOWERCASE_ALPHANUMERIC = string.lowercase + string.digits
     31 UPPERCASE_ALPHANUMERIC = string.uppercase + string.digits
     32 ALPHA = string.letters
     33 ALPHANUMERIC = string.letters + string.digits
     34 ASCII_PRINTABLE = string.letters + string.digits + string.punctuation
     35 ALL_PRINTABLE = string.printable
     36 PUNCTUATION = string.punctuation
     37 
     38 
     39 def generate_random_string(length=None, entropy=None, pool=ALPHANUMERIC):
     40     """Generates a random string using the given sequence pool.
     41 
     42     To generate stronger passwords, use ASCII_PRINTABLE as pool.
     43 
     44     Entropy is:
     45 
     46          H = log2(N**L)
     47 
     48     where:
     49 
     50     - H is the entropy in bits.
     51     - N is the possible symbol count
     52     - L is length of string of symbols
     53 
     54     Entropy chart::
     55 
     56         -----------------------------------------------------------------
     57         Symbol set              Symbol Count (N)  Entropy per symbol (H)
     58         -----------------------------------------------------------------
     59         HEXADECIMAL_DIGITS      16                4.0000 bits
     60         DIGITS                  10                3.3219 bits
     61         LOWERCASE_ALPHA         26                4.7004 bits
     62         UPPERCASE_ALPHA         26                4.7004 bits
     63         PUNCTUATION             32                5.0000 bits
     64         LOWERCASE_ALPHANUMERIC  36                5.1699 bits
     65         UPPERCASE_ALPHANUMERIC  36                5.1699 bits
     66         ALPHA                   52                5.7004 bits
     67         ALPHANUMERIC            62                5.9542 bits
     68         ASCII_PRINTABLE         94                6.5546 bits
     69         ALL_PRINTABLE           100               6.6438 bits
     70 
     71     :param length:
     72         The length of the random sequence. Use this or `entropy`, not both.
     73     :param entropy:
     74         Desired entropy in bits. Use this or `length`, not both.
     75         Use this to generate passwords based on entropy:
     76         http://en.wikipedia.org/wiki/Password_strength
     77     :param pool:
     78         A sequence of characters from which random characters are chosen.
     79         Default to case-sensitive alpha-numeric characters.
     80     :returns:
     81         A string with characters randomly chosen from the pool.
     82     """
     83     pool = list(set(pool))
     84 
     85     if length and entropy:
     86         raise ValueError('Use length or entropy, not both.')
     87 
     88     if length <= 0 and entropy <= 0:
     89         raise ValueError('Length or entropy must be greater than 0.')
     90 
     91     if entropy:
     92         log_of_2 = 0.6931471805599453
     93         length = long(math.ceil((log_of_2 / math.log(len(pool))) * entropy))
     94 
     95     return ''.join(_rng.choice(pool) for _ in xrange(length))
     96 
     97 
     98 def generate_password_hash(password, method='sha1', length=22, pepper=None):
     99     """Hashes a password.
    100 
    101     The format of the string returned includes the method that was used
    102     so that :func:`check_password_hash` can check the hash.
    103 
    104     This method can **not** generate unsalted passwords but it is possible
    105     to set the method to plain to enforce plaintext passwords. If a salt
    106     is used, hmac is used internally to salt the password.
    107 
    108     :param password:
    109         The password to hash.
    110     :param method:
    111         The hash method to use (``'md5'`` or ``'sha1'``).
    112     :param length:
    113         Length of the salt to be created.
    114     :param pepper:
    115         A secret constant stored in the application code.
    116     :returns:
    117         A formatted hashed string that looks like this::
    118 
    119             method$salt$hash
    120 
    121     This function was ported and adapted from `Werkzeug`_.
    122     """
    123     salt = method != 'plain' and generate_random_string(length) or ''
    124     hashval = hash_password(password, method, salt, pepper)
    125     if hashval is None:
    126         raise TypeError('Invalid method %r.' % method)
    127 
    128     return '%s$%s$%s' % (hashval, method, salt)
    129 
    130 
    131 def check_password_hash(password, pwhash, pepper=None):
    132     """Checks a password against a given salted and hashed password value.
    133 
    134     In order to support unsalted legacy passwords this method supports
    135     plain text passwords, md5 and sha1 hashes (both salted and unsalted).
    136 
    137     :param password:
    138         The plaintext password to compare against the hash.
    139     :param pwhash:
    140         A hashed string like returned by :func:`generate_password_hash`.
    141     :param pepper:
    142         A secret constant stored in the application code.
    143     :returns:
    144         `True` if the password matched, `False` otherwise.
    145 
    146     This function was ported and adapted from `Werkzeug`_.
    147     """
    148     if pwhash.count('$') < 2:
    149         return False
    150 
    151     hashval, method, salt = pwhash.split('$', 2)
    152     return hash_password(password, method, salt, pepper) == hashval
    153 
    154 
    155 def hash_password(password, method, salt=None, pepper=None):
    156     """Hashes a password.
    157 
    158     Supports plaintext without salt, unsalted and salted passwords. In case
    159     salted passwords are used hmac is used.
    160 
    161     :param password:
    162         The password to be hashed.
    163     :param method:
    164         A method from ``hashlib``, e.g., `sha1` or `md5`, or `plain`.
    165     :param salt:
    166         A random salt string.
    167     :param pepper:
    168         A secret constant stored in the application code.
    169     :returns:
    170         A hashed password.
    171 
    172     This function was ported and adapted from `Werkzeug`_.
    173     """
    174     password = webapp2._to_utf8(password)
    175     if method == 'plain':
    176         return password
    177 
    178     method = getattr(hashlib, method, None)
    179     if not method:
    180         return None
    181 
    182     if salt:
    183         h = hmac.new(webapp2._to_utf8(salt), password, method)
    184     else:
    185         h = method(password)
    186 
    187     if pepper:
    188         h = hmac.new(webapp2._to_utf8(pepper), h.hexdigest(), method)
    189 
    190     return h.hexdigest()
    191 
    192 
    193 def compare_hashes(a, b):
    194     """Checks if two hash strings are identical.
    195 
    196     The intention is to make the running time be less dependant on the size of
    197     the string.
    198 
    199     :param a:
    200         String 1.
    201     :param b:
    202         String 2.
    203     :returns:
    204         True if both strings are equal, False otherwise.
    205     """
    206     if len(a) != len(b):
    207         return False
    208 
    209     result = 0
    210     for x, y in zip(a, b):
    211         result |= ord(x) ^ ord(y)
    212 
    213     return result == 0
    214 
    215 
    216 # Old names.
    217 create_token = generate_random_string
    218 create_password_hash = generate_password_hash
    219