Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 158 lines (122 sloc) 5.602 kb
8512d92 @tallowen Use django password hashers in django 1.4
tallowen authored
1 import base64
2 import hmac
3 import hashlib
4 import logging
5
6 import bcrypt
7
8 from django.conf import settings
9 from django.contrib.auth.hashers import (BCryptPasswordHasher,
10 BasePasswordHasher, mask_hash)
11 from django.utils.crypto import constant_time_compare
12 from django.utils.encoding import smart_str
13 from django.utils.datastructures import SortedDict
14
15 log = logging.getLogger('common.hashers')
16
17 algo_name = lambda hmac_id: 'bcrypt{0}'.format(hmac_id.replace('-', '_'))
18
19
20 def get_hasher(hmac_id):
21 """
22 Dynamically create password hashers based on hmac_id.
23
24 This class takes the hmac_id corresponding to an HMAC_KEY and creates a
25 password hasher class based off of it. This allows us to use djangos
26 built-in updating mechanisms to automatically update the HMAC KEYS.
27 """
28 dash_hmac_id = hmac_id.replace('_', '-')
29
30 class BcryptHMACPasswordHasher(BCryptPasswordHasher):
31 algorithm = algo_name(hmac_id)
32 rounds = getattr(settings, 'BCRYPT_ROUNDS', 12)
33
34 def encode(self, password, salt):
35
36 shared_key = settings.HMAC_KEYS[dash_hmac_id]
37
38 hmac_value = self._hmac_create(password, shared_key)
39 bcrypt_value = bcrypt.hashpw(hmac_value, salt)
40 return '{0}{1}${2}'.format(
41 self.algorithm,
42 bcrypt_value,
43 dash_hmac_id)
44
45 def verify(self, password, encoded):
46 algo_and_hash, key_ver = encoded.rsplit('$', 1)
47 try:
48 shared_key = settings.HMAC_KEYS[key_ver]
49 except KeyError:
50 log.info('Invalid shared key version "{0}"'.format(key_ver))
51 return False
52
53 bc_value = '${0}'.format(algo_and_hash.split('$', 1)[1]) # Yes, bcrypt <3s the leading $.
54 hmac_value = self._hmac_create(password, shared_key)
55 return bcrypt.hashpw(hmac_value, bc_value) == bc_value
56
57 def _hmac_create(self, password, shared_key):
58 """Create HMAC value based on pwd"""
59 hmac_value = base64.b64encode(hmac.new(
60 smart_str(shared_key),
61 smart_str(password),
62 hashlib.sha512).digest())
63 return hmac_value
64
65 return BcryptHMACPasswordHasher
66
67 # We must have HMAC_KEYS. If not, let's raise an import error.
68 if not settings.HMAC_KEYS:
69 raise ImportError('settings.HMAC_KEYS must not be empty.')
70
71 # For each HMAC_KEY, dynamically create a hasher to be imported.
72 for hmac_key in settings.HMAC_KEYS.keys():
73 hmac_id = hmac_key.replace('-', '_')
74 globals()[algo_name(hmac_id)] = get_hasher(hmac_id)
75
76
77 class BcryptHMACCombinedPasswordVerifier(BCryptPasswordHasher):
78 """
79 This reads anything with 'bcrypt' as the algo. This should be used
80 to read bcypt values (with or without HMAC) in order to re-encode them
81 as something else.
82 """
83 algorithm = 'bcrypt'
84 rounds = getattr(settings, 'BCRYPT_ROUNDS', 12)
85
86 def encode(self, password, salt):
87 """This hasher is not meant to be used for encoding"""
88 raise NotImplementedError()
89
90 def verify(self, password, encoded):
91 algo_and_hash, key_ver = encoded.rsplit('$', 1)
92 try:
93 shared_key = settings.HMAC_KEYS[key_ver]
94 except KeyError:
95 log.info('Invalid shared key version "{0}"'.format(key_ver))
96 # Fall back to normal bcrypt
97 algorithm, data = encoded.split('$', 1)
98 return constant_time_compare(data, bcrypt.hashpw(password, data))
99
100 bc_value = '${0}'.format(algo_and_hash.split('$', 1)[1]) # Yes, bcrypt <3s the leading $.
101 hmac_value = self._hmac_create(password, shared_key)
102 return bcrypt.hashpw(hmac_value, bc_value) == bc_value
103
104 def _hmac_create(self, password, shared_key):
105 """Create HMAC value based on pwd"""
106 hmac_value = base64.b64encode(hmac.new(
107 smart_str(shared_key),
108 smart_str(password),
109 hashlib.sha512).digest())
110 return hmac_value
111
112
113 class SHA256PasswordHasher(BasePasswordHasher):
114 """The SHA256 password hashing algorithm."""
115 algorithm = 'sha256'
116
117 def encode(self, password, salt):
118 assert password
119 assert salt and '$' not in salt
120 hash = getattr(hashlib, self.algorithm)(salt + password).hexdigest()
121 return '%s$%s$%s' % (self.algorithm, salt, hash)
122
123 def verify(self, password, encoded):
124 algorithm, salt, hash = encoded.split('$', 2)
125 assert algorithm == self.algorithm
126 encoded_2 = self.encode(password, salt)
127 return constant_time_compare(encoded, encoded_2)
128
129 def safe_summary(self, encoded):
130 algorithm, salt, hash = encoded.split('$', 2)
131 assert algorithm == self.algorithm
132 return SortedDict([
133 ('algorithm', algorithm),
134 ('salt', mask_hash(salt, show=2)),
135 ('hash', mask_hash(hash)),
136 ])
137
138
139 class SHA1PasswordHasher(SHA256PasswordHasher):
140 """The SHA1 password hashing algorithm."""
141 algorithm = 'sha1'
142
143
144 class SHA512PasswordHasher(SHA256PasswordHasher):
145 """The SHA512 password hashing algorithm."""
146 algorithm = 'sha512'
147
148
149 class SHA512b64PasswordHasher(SHA512PasswordHasher):
150 """The SHA512 password hashing algorithm with base64 encoding."""
151 algorithm = 'sha512b64'
152
153 def encode(self, password, salt):
154 assert password
155 assert salt and '$' not in salt
156 hash = base64.encodestring(hashlib.sha512(salt + password).digest())
157 return '%s$%s$%s' % (self.algorithm, salt, hash)
Something went wrong with that request. Please try again.