8ad765e023
Support bcrypt, pbkdf2_sha512, or scrypt in password hashing for passwords managed within keystone. sha512_crypt is insufficient to hash passwords in a secure way for storage in the DB. Keystone defaults now to using bcrypt but can handle scrypt and pbkdf2_sha512 with a number of tuning options if desired. Closes-bug: #1543048 Closes-bug: #1668503 Change-Id: Id05026720839d94de26d0e44631deb34bcc0e610
131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
# Copyright 2017 Red Hat
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import itertools
|
|
|
|
from oslo_log import log
|
|
import passlib.hash
|
|
|
|
import keystone.conf
|
|
from keystone import exception
|
|
from keystone.i18n import _
|
|
|
|
|
|
CONF = keystone.conf.CONF
|
|
LOG = log.getLogger(__name__)
|
|
|
|
SUPPORTED_HASHERS = frozenset([passlib.hash.bcrypt,
|
|
passlib.hash.scrypt,
|
|
passlib.hash.pbkdf2_sha512,
|
|
passlib.hash.sha512_crypt])
|
|
|
|
_HASHER_NAME_MAP = {hasher.name: hasher for hasher in SUPPORTED_HASHERS}
|
|
|
|
|
|
# NOTE(notmorgan): Build the list of prefixes. This comprehension builds
|
|
# a dictionary where the keys are the prefix (all hashedpasswords are
|
|
# '$<ident>$<metadata>$<hash>') so we can do a fast-lookup on the hasher to
|
|
# use. If has hasher has multiple ident options it is encoded in the
|
|
# .ident_values attribute whereas hashers that have a single option
|
|
# (sha512_crypt) only has the .ident attribute.
|
|
_HASHER_IDENT_MAP = {
|
|
prefix: module for module, prefix in itertools.chain(
|
|
*[zip([mod] * len(getattr(mod, 'ident_values', (mod.ident,))),
|
|
getattr(mod, 'ident_values', (mod.ident,)))
|
|
for mod in SUPPORTED_HASHERS])}
|
|
|
|
|
|
def _get_hasher_from_ident(hashed):
|
|
try:
|
|
return _HASHER_IDENT_MAP[hashed[0:hashed.index('$', 1) + 1]]
|
|
except KeyError:
|
|
raise ValueError(
|
|
_('Unsupported password hashing algorithm ident: %s') %
|
|
hashed[0:hashed.index('$', 1) + 1])
|
|
|
|
|
|
def verify_length_and_trunc_password(password):
|
|
"""Verify and truncate the provided password to the max_password_length."""
|
|
max_length = CONF.identity.max_password_length
|
|
try:
|
|
if len(password) > max_length:
|
|
if CONF.strict_password_check:
|
|
raise exception.PasswordVerificationError(size=max_length)
|
|
else:
|
|
msg = "Truncating user password to %d characters."
|
|
LOG.warning(msg, max_length)
|
|
return password[:max_length]
|
|
else:
|
|
return password
|
|
except TypeError:
|
|
raise exception.ValidationError(attribute='string', target='password')
|
|
|
|
|
|
def check_password(password, hashed):
|
|
"""Check that a plaintext password matches hashed.
|
|
|
|
hashpw returns the salt value concatenated with the actual hash value.
|
|
It extracts the actual salt if this value is then passed as the salt.
|
|
|
|
"""
|
|
if password is None or hashed is None:
|
|
return False
|
|
password_utf8 = verify_length_and_trunc_password(password).encode('utf-8')
|
|
hasher = _get_hasher_from_ident(hashed)
|
|
return hasher.verify(password_utf8, hashed)
|
|
|
|
|
|
def hash_user_password(user):
|
|
"""Hash a user dict's password without modifying the passed-in dict."""
|
|
password = user.get('password')
|
|
if password is None:
|
|
return user
|
|
|
|
return dict(user, password=hash_password(password))
|
|
|
|
|
|
def hash_password_compat(password):
|
|
password_utf8 = verify_length_and_trunc_password(password).encode('utf-8')
|
|
return passlib.hash.sha512_crypt.using(rounds=CONF.crypt_strength).hash(
|
|
password_utf8)
|
|
|
|
|
|
def hash_password(password):
|
|
"""Hash a password. Harder."""
|
|
params = {}
|
|
password_utf8 = verify_length_and_trunc_password(password).encode('utf-8')
|
|
conf_hasher = CONF.identity.password_hash_algorithm
|
|
hasher = _HASHER_NAME_MAP.get(conf_hasher)
|
|
|
|
if hasher is None:
|
|
raise RuntimeError(
|
|
_('Password Hash Algorithm %s not found') %
|
|
CONF.identity.password_hash_algorithm)
|
|
|
|
if CONF.identity.password_hash_rounds:
|
|
params['rounds'] = CONF.identity.password_hash_rounds
|
|
if hasher is passlib.hash.scrypt:
|
|
if CONF.identity.scrypt_block_size:
|
|
params['block_size'] = CONF.identity.scrypt_block_size
|
|
if CONF.identity.scrypt_parallelism:
|
|
params['parallelism'] = CONF.identity.scrypt_parallelism
|
|
if CONF.identity.salt_bytesize:
|
|
params['salt_size'] = CONF.identity.salt_bytesize
|
|
if hasher is passlib.hash.pbkdf2_sha512:
|
|
if CONF.identity.salt_bytesize:
|
|
params['salt_size'] = CONF.identity.salt_bytesize
|
|
|
|
return hasher.using(**params).hash(password_utf8)
|