I am working on the migration of one website with php to Django framework.
There is used to a specific hash passwords algorithm, so I had to write:
#settings.py
PASSWORD_HASHERS = (
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'project.hashers.SHA1ProjPasswordHasher', # that's mine
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
...
)
and:
#hashers.py
import hashlib
from django.contrib.auth.hashers import (BasePasswordHasher, mask_hash)
from django.utils.datastructures import SortedDict
from django.utils.encoding import force_bytes
from django.utils.crypto import constant_time_compare
from django.utils.translation import ugettext_noop as _
class SHA1ProjPasswordHasher(BasePasswordHasher):
"""
Special snowflake algorithm from the first version.
php code: $pass=substr(sha1(trim($_POST['password'])),0,8);
"""
algorithm = "unsalted_and_trimmed_sha1"
def salt(self):
return ''
def encode(self, password, salt):
return hashlib.sha1(force_bytes(salt + password)).hexdigest()[:8]
def verify(self, password, encoded):
encoded_2 = self.encode(password, '')
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
return SortedDict([
(_('algorithm'), self.algorithm),
(_('hash'), mask_hash(encoded, show=3)),
])
It's works well when PBKDF2PasswordHasher is first:
>>> from django.contrib.auth import authenticate
>>> u = authenticate(username='root', password='test')
>>> u.password
u'pbkdf2_sha256$10000$EX8BcgPFjygx$HvB6NmZ7uX1rWOOPbHRKd8GLYD3cAsQtlprXUq1KGMk='
>>> exit()
Then I put my SHA1ProjPasswordHasher on the first place, first authentication works great. The hash was changed.:
>>> from django.contrib.auth import authenticate
>>> u = authenticate(username='root', password='test')
>>> u.password
'a94a8fe5'
>>> exit()
Second authentication is failed. Can't authenticate with new hash.
>>> from django.contrib.auth import authenticate
>>> u = authenticate(username='root', password='test')
>>> u.password
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'password'
What could be the problem? Thanks.
UPDATE: Ok, the problem became more clear. When I remove slice from here:
return hashlib.sha1(force_bytes(salt + password)).hexdigest()[:8]
everything working fine. I can't get why..
Although years have passed, i' m putting my solution here for future reference
Vlad is partially right; The following method from django.contrib.auth.hashers seems to be forcing you to use a hash format that includes the dollar $ sign to mark the algorithm used for django to decide which hasher to use
def identify_hasher(encoded):
"""
Returns an instance of a loaded password hasher.
Identifies hasher algorithm by examining encoded hash, and calls
get_hasher() to return hasher. Raises ValueError if
algorithm cannot be identified, or if hasher is not loaded.
"""
# Ancient versions of Django created plain MD5 passwords and accepted
# MD5 passwords with an empty salt.
if ((len(encoded) == 32 and '$' not in encoded) or
(len(encoded) == 37 and encoded.startswith('md5$$'))):
algorithm = 'unsalted_md5'
# Ancient versions of Django accepted SHA1 passwords with an empty salt.
elif len(encoded) == 46 and encoded.startswith('sha1$$'):
algorithm = 'unsalted_sha1'
else:
algorithm = encoded.split('$', 1)[0]
return get_hasher(algorithm)
There is a way though to "trick" django without hacking your django installation. You 'll have to create an authentication backend to use for your authentication. There you'll override django's check_password method. I had a db where hashes were {SSHA512}hash and i couldn't change this because i had to be able to communicate with dovecot. So i put the following in my backends.py class:
def check_password(self, raw_password, user):
"""
Returns a boolean of whether the raw_password was correct. Handles
hashing formats behind the scenes.
"""
def setter(raw_password):
user.set_password(raw_password)
user.save(update_fields=["password"])
return check_password(raw_password, "SSHA512$" + user.password, setter)
That way when django has to check if a password is correct it'll do the following:
-Get the hash from the db {SSHA512}hash
-Append it temporarily a SSHA512$ string on the beginning and then check
So while you'll be having {SSHA512}hash in your database, when django is using this backend it'll see SSHA512${SSHA512}hash.
This way in your hashers.py you can set in your class algorithm = "SSHA512" which will hint django to use this hasher for that case.
Your def encode(self, password, salt, iterations=None) method in your hashers.py will save hashes the way dovecot needs {SSHA512}hash (you don't have to do anything weird in your encode method).
Your def verify(self, password, encoded) method though will have to strip the SSHA512$ "trick" from the encoded string that it is passed to compare it with the one that encode will create.
So there you have it! Django will use your hasher for checking hashes that don't contain a dollar $ sign and you don't have to break anything inside django :)
Only unsalted md5 hashes can not include a dollar sign:
# django/contrib/auth/hashers.py
def identify_hasher(encoded):
"""
Returns an instance of a loaded password hasher.
Identifies hasher algorithm by examining encoded hash, and calls
get_hasher() to return hasher. Raises ValueError if
algorithm cannot be identified, or if hasher is not loaded.
"""
if len(encoded) == 32 and '$' not in encoded:
algorithm = 'unsalted_md5'
else:
algorithm = encoded.split('$', 1)[0]
return get_hasher(algorithm)
So the best way is convert the current password hashes to the format: alg$salt$hash
class SHA1ProjPasswordHasher(BasePasswordHasher):
"""
Special snowflake algorithm from the first version.
php code: $pass=substr(sha1(trim($_POST['password'])),0,8);
"""
algorithm = "unsalted_and_trimmed_sha1"
def salt(self):
return ''
def encode(self, password, salt):
assert password
assert '$' not in salt
hash = hashlib.sha1(force_bytes(salt + password)).hexdigest()[:8]
return "%s$%s$%s" % (self.algorithm, salt, hash)
def verify(self, password, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
encoded_2 = self.encode(password, salt)
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
return SortedDict([
(_('algorithm'), algorithm),
(_('salt'), mask_hash(salt, show=2)),
(_('hash'), mask_hash(hash)),
])
.
>>> from django.contrib.auth import authenticate
>>> x = authenticate(username='root', password='test')
>>> x
<User: root>
>>> x.password
u'unsalted_and_trimmed_sha1$$a94a8fe5'
Related
I am migrating an old system that uses unsalted MD5 passwords (the horror!).
I know Django handles automatically password upgrading, as users log in, by adding additional hashers to the PASSWORD_HASHERS list in settings.py.
But, I would like to upgrade the passwords without requiring users to log in, also explained in the docs.
So, I've followed the example in the docs and implemented a custom hasher, legacy/hasher.py:
import secrets
from django.contrib.auth.hashers import PBKDF2PasswordHasher, UnsaltedMD5PasswordHasher
class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
algorithm = "pbkdf2_wrapped_md5"
def encode_md5_hash(self, md5_hash):
salt = secrets.token_hex(16)
return super().encode(md5_hash, salt)
def encode(self, password, salt, iterations=None):
md5_hash = UnsaltedMD5PasswordHasher().encode(password, salt="")
return self.encode_md5_hash(md5_hash)
and add it to settings.py:
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"legacy.hashers.PBKDF2WrappedMD5PasswordHasher",
]
However, testing this in the Django shell check_password is returning False for the upgraded password.
>>> from django.contrib.auth.hashers import check_password, UnsaltedMD5PasswordHasher
>>> from legacy.hashers import PBKDF2WrappedMD5PasswordHasher
>>> hasher = PBKDF2WrappedMD5PasswordHasher()
>>> test_pwd = '123456'
>>> test_pwd_unsalted_md5 = UnsaltedMD5PasswordHasher().encode(test_pwd, salt='')
>>> print(test_pwd_unsalted_md5)
'827ccb0eea8a706c4c34a16891f84e7b' # this is an example of a password I want to upgrade
>>> upgraded_test_pwd = hasher.encode_md5_hash(test_pwd)
>>> print(upgraded_test_pwd)
pbkdf2_wrapped_md5$150000$f3aae83b02e8727a2477644eb0aa6560$brqCWW5QuGUoSQ28YNPGUwTLEwZOuMNheN2RxVZGtHQ=
>>> check_password(test_pwd, upgraded_test_pwd)
False
I've looked into other similar SO questions, but didn't found a proper solution there as well.
Short answer: by not taking the provided salt into account, when verifying Django can not (likely) come up with the same encoded password.
The reason this happens is because you generate "salt" out of thin air, and ignore the salt that is passed. Indeed, if we take a look at your implementation, we see:
class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
algorithm = "pbkdf2_wrapped_md5"
def encode_md5_hash(self, md5_hash):
salt = secrets.token_hex(16) # generating random salt
return super().encode(md5_hash, salt)
def encode(self, password, salt, iterations=None):
md5_hash = UnsaltedMD5PasswordHasher().encode(password, salt='')
return self.encode_md5_hash(md5_hash)
The salt that is passed to the encode(..) method is thus ignored.
This means that if you later want to verify the password, Django will call encode(..) with the salt that it stored (in your case, that is the second part of the encoded password, so f3aae83b02e8727a2477644eb0aa6560), but you decide to throw that away, and generate the password with different salt, and therefore the encoded password, does no longer match with the password you did store in the database.
I advice to use the salt, for example with:
class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
algorithm = "pbkdf2_wrapped_md5"
def encode_md5_hash(self, md5_hash, salt):
return super().encode(md5_hash, salt)
def encode(self, password, salt, iterations=None):
md5_hash = UnsaltedMD5PasswordHasher().encode(password, salt='')
return self.encode_md5_hash(md5_hash, salt)
I'm having problems using Flask-JWT in my application, I am using the authenticate and identity like this:
def authenticate(username, password):
session = db.Session()
user = session.query(db.Operators).filter_by(username= username).first()
if user and bcrypt.check_password_hash(user.password, password):
return user
def identity(payload):
user_id = payload['identity']
session = db.Session()
return session.query(db.Operators).filter_by(idOperator= user_id)
But I get an error because I do not have an id field in my db table because I have an idOperator
How can I solve this problem? The _default_jwt_payload_handler(identity) function goes to seek for an Id field, how can I change this automatic id field to an IdOperator without changing the init.py of flask-jwt?
Thanks
You can use the jwt_payload_handler decorator to specify your own payload handler.
For example, mimicing the default behavior but instead using idOperator could look like this:
jwt = JWT(app, authenticate, identity)
#jwt.jwt_payload_handler
def make_payload(identity):
iat = datetime.utcnow()
exp = iat + current_app.config.get('JWT_EXPIRATION_DELTA')
nbf = iat + current_app.config.get('JWT_NOT_BEFORE_DELTA')
identity = getattr(identity, 'idOperator') or identity['idOperator']
return {'exp': exp, 'iat': iat, 'nbf': nbf, 'identity': identity}
Which uses:
from datetime import datetime
from flask import current_app
Here is the documentations specification.
You might want to consider flask-jwt-extended instead. Flask-JWT has been abandoned for years now, whereas flask-jwt-extended is still actively maintained and much easier to customize: https://flask-jwt-extended.readthedocs.io/en/latest/basic_usage.html
I am following the instruction from this page. I am building a slack slash command handling server and I can't rebuild the signature to validate slash request authenticity.
here is the code snippet from my django application (the view uses the django rest-framework APIView):
#property
def x_slack_req_ts(self):
if self.xsrts is not None:
return self.xsrts
self.xsrts = str(self.request.META['HTTP_X_SLACK_REQUEST_TIMESTAMP'])
return self.xsrts
#property
def x_slack_signature(self):
if self.xss is not None:
return self.xss
self.xss = self.request.META['HTTP_X_SLACK_SIGNATURE']
return self.xss
#property
def base_message(self):
if self.bs is not None:
return self.bs
self.bs = ':'.join(["v0", self.x_slack_req_ts, self.raw.decode('utf-8')])
return self.bs
#property
def encoded_secret(self):
return self.app.signing_secret.encode('utf-8')
#property
def signed(self):
if self.non_base is not None:
return self.non_base
hashed = hmac.new(self.encoded_secret, self.base_message.encode('utf-8'), hashlib.sha256)
self.non_base = "v0=" + hashed.hexdigest()
return self.non_base
This is within a class where self.raw = request.body the django request and self.app.signing_secret is a string with the appropriate slack secret string. It doesn't work as the self.non_base yield an innaccurate value.
Now if I open an interactive python repl and do the following:
>>> import hmac
>>> import hashlib
>>> secret = "8f742231b10e8888abcd99yyyzzz85a5"
>>> ts = "1531420618"
>>> msg = "token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c"
>>> ref_signature = "v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503"
>>> base = ":".join(["v0", ts, msg])
>>> hashed = hmac.new(secret.encode(), base.encode(), hashlib.sha256)
>>> hashed.hexdigest()
>>> 'a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503'
You will recognise the referenced link example. If I use the values from my django app with one of MY examples, it works within the repl but doesn't within the django app.
MY QUESTION: I believe this is caused by the self.raw.decode() encoding not being consistent with the printout I extracted to copy/paste in the repl. Has anyone encountered that issue and what is the fix? I tried a few random things with the urllib.parse library... How can I make sure that the request.body encoding is consistent with the example from flask with get_data() (as suggested by the doc in the link)?
UPDATE: I defined a custom parser:
class SlashParser(BaseParser):
"""
Parser for form data.
"""
media_type = 'application/x-www-form-urlencoded'
def parse(self, stream, media_type=None, parser_context=None):
"""
Parses the incoming bytestream as a URL encoded form,
and returns the resulting QueryDict.
"""
parser_context = parser_context or {}
request = parser_context.get('request')
raw_data = stream.read()
data = QueryDict(raw_data, encoding='utf-8')
setattr(data, 'raw_body', raw_data) # setting a 'body' alike custom attr with raw POST content
return data
To test based on this question and the raw_body in the custom parser generates the exact same hashed signature as the normal "body" but again, copy pasting in the repl to test outside the DRF works. Pretty sure it's an encoding problem but completely at loss...
I found the problem which is very frustrating.
It turns out that the signing secret was stored in too short a str array and were missing trailing characters which obviously, resulted in bad hashing of the message.
I migrated users from other site to django site. Old web page had md5 passwords with salt and I am writing a custom hashing alghorithm to so users may authenticate with their old passwords.
When I am trying to login as old user I get:
Unknown password hashing algorithm 'my_hashed_password'. Did you specify it in the PASSWORD_HASHERS setting?
Thats my password hasher
class FallbackMD5PasswordHasher(BasePasswordHasher):
algorithm = "fallback_md5"
def salt(self):
return 'my_salt'
def encode(self, password):
return hashlib.md5(self.salt() + password).hexdigest()
def verify(self, password, encoded):
encoded_2 = self.encode(password, '')
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
return SortedDict([
(_('algorithm'), self.algorithm),
(_('hash'), mask_hash(encoded, show=3)),
])
Thats my settings.py
PASSWORD_HASHERS = (
'lfs_custom.hasher.FallbackMD5PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher',
'django.contrib.auth.hashers.CryptPasswordHasher',
)
Please provide me some info how to fix this :) I can't find it out why its not working.
Django tries to parse hash from your database using this format:
<algorithm>$<iterations>$<salt>$<hash>
You likely have plain hashes in database. Try to execute an SQL on database to update with correct format, something like:
update auth_user set password=concat("md5$1$", password);
if you need this
{
class FallbackMD5PasswordHasher(BasePasswordHasher):
algorithm = "fallback_md5"
iterations = 100000
def salt(self):
return ''
def encode(self, password, salt):
assert salt == ''
hash= hashlib.md5(password).hexdigest()
return "%s$%d$%s$%s" % (self.algorithm, self.iterations, '', hash)
def verify(self, password, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3)
assert algorithm == self.algorithm
encoded_2 = self.encode(password, '')
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3)
return SortedDict([
(_('algorithm'), algorithm),
(_('iterations'), iterations),
(_('salt'), mask_hash(salt)),
(_('hash'), mask_hash(hash)),
])
def must_update(self, encoded):
return True
}
I have imported a heap of users and their data to a django project. I need to assign a password to each. Is the such a snippet out there for password generation that will cope with the Django hash and salt?
You can also use the built in function make_random_password
for user in new_users:
password = User.objects.make_random_password()
user.set_password(password)
user.save(update_fields=['password'])
# email/print password
Also you can use from django.utils.crypto import get_random_string out of auth module, it accepts keyword arguments length and allowed_chars as well.
If you need only a Django`s solutions, then try next:
For generate a normal password try use BaseUserManager.
In [341]: from django.contrib.auth.base_user import BaseUserManager
# simple password, it length is 10, and it contains ascii letters and digits
In [344]: BaseUserManager().make_random_password()
Out[344]: 'aYMX5Wk7Cu'
In [345]: BaseUserManager().make_random_password()
Out[345]: 'rM7759hw96'
In [346]: BaseUserManager().make_random_password()
Out[346]: 'EkbZxEXyAm'
# passed length of a password
In [347]: BaseUserManager().make_random_password(45)
Out[347]: 'dtM9vhSBL9WSFeEdPqj8jVPMH9ytsjPXrkaHUNUQu4zVH'
In [348]: BaseUserManager().make_random_password(45)
Out[348]: 'jypVaXuw9Uw8mD4CXtEhtj2E4DVYx23YTMwy8jGTKsreR'
# passed length of a password and symbols for choice
In [349]: BaseUserManager().make_random_password(45, 'abcdf')
Out[349]: 'daacbfabfccfdbdddbbcddcfcfbfcdabbaccbfcadbccd'
In [351]: import string
# password contains only digits
In [352]: BaseUserManager().make_random_password(50, string.digits)
Out[352]: '00526693878168774026398080457185060971935025500935'
# password contains only ascii symbols in lowercase
In [353]: BaseUserManager().make_random_password(50, string.ascii_lowercase)
Out[353]: 'nvftisuezofnashdhlalfmscnmqtvigwjpfwsyycsefekytmar'
# password contains only ascii symbols in uppercase
In [354]: BaseUserManager().make_random_password(50, string.ascii_uppercase)
Out[354]: 'APKSUHHHTAAJCFEUONIXWWAKJGXIBHTQDZBTSYFTPDFOSRYEQR'
If you need strong and power password, then try built-in "hashers" in the Django
In [355]: from django.contrib.auth.hashers import make_password
In [357]: make_password('')
Out[357]: 'pbkdf2_sha256$30000$JuKXdW3shCjL$PsPJX7Zale5JUBkWpIJI/+QlsuVWhz9Q+GQWVtTpQ/Y='
In [358]: make_password('text')
Out[358]: 'pbkdf2_sha256$30000$lSv8kQ39BHE7$KQC5hRhuphYBXmBrXZBJGC+nxygfNWTDf8zQf/NNgY8='
In [360]: make_password('text', salt=['simething'])
Out[360]: "pbkdf2_sha256$30000$['simething']$D+1vJQx9W2/c9sIz/J+7iEz4d4KFPg/R+0S87n/RKR4="
In [361]: make_password('text', salt=['something'])
Out[361]: "pbkdf2_sha256$30000$['something']$NIcmOkEyg6mnH5Ljt+KvI2LVgZWg6sXS6Rh865rbhSc="
Notes:
Used Django 1.10 and Python 3.4
Just use the API - django.contrib.auth.models.User has a .set_password() method. Here's an example (that I haven't tested, but you should get the idea):
from random import choice
from string import digits, letters
from django.contrib.auth.models import User
def _pw(length=6):
s = ''
for i in range(length):
s += random.choice(digits + letters)
return s
for user in User.objects.all(): # or .filter(...)
user.set_password(_pw())
user.save()
import random
import string
user.set_password(''.join([random.choice(string.digits + string.letters) for i in range(0, 10)]))
user.save()