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@gmail.com>
12    :license: Apache Sotware License, see LICENSE for details.
13"""
14from __future__ import division
15
16import hashlib
17import hmac
18import math
19import random
20import string
21
22import webapp2
23
24_rng = random.SystemRandom()
25
26HEXADECIMAL_DIGITS = string.digits + 'abcdef'
27DIGITS = string.digits
28LOWERCASE_ALPHA = string.lowercase
29UPPERCASE_ALPHA = string.uppercase
30LOWERCASE_ALPHANUMERIC = string.lowercase + string.digits
31UPPERCASE_ALPHANUMERIC = string.uppercase + string.digits
32ALPHA = string.letters
33ALPHANUMERIC = string.letters + string.digits
34ASCII_PRINTABLE = string.letters + string.digits + string.punctuation
35ALL_PRINTABLE = string.printable
36PUNCTUATION = string.punctuation
37
38
39def 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
98def 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
131def 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
155def 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
193def 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.
217create_token = generate_random_string
218create_password_hash = generate_password_hash
219