ReAnzu 3f52aceca4 server/users: harden password hashes
- Changed password setup to use libsodium and argon2id (regular SHA256
  hashing for passwords is inadequate as modern GPU's can hash generate
  billions of hashes per second).
- Added code to auto migrate old passwords to the new password_hash if
  the existing password_hash matches either of the legacy password
  generation schemes (SHA1 or SHA256).
- Added migration to support new password_hash format length
- Added column password_revision. This field will default to 0, which
  all passwords will have till they're updated. After that each password
  hash method has a revision.
2018-03-08 23:40:47 +01:00

105 lines
3.4 KiB
Python

from typing import Tuple
import hashlib
import random
from collections import OrderedDict
from nacl import pwhash
from nacl.exceptions import InvalidkeyError
from szurubooru import config, model, errors, db
from szurubooru.func import util
RANK_MAP = OrderedDict([
(model.User.RANK_ANONYMOUS, 'anonymous'),
(model.User.RANK_RESTRICTED, 'restricted'),
(model.User.RANK_REGULAR, 'regular'),
(model.User.RANK_POWER, 'power'),
(model.User.RANK_MODERATOR, 'moderator'),
(model.User.RANK_ADMINISTRATOR, 'administrator'),
(model.User.RANK_NOBODY, 'nobody'),
])
def get_password_hash(salt: str, password: str) -> Tuple[str, int]:
''' Retrieve argon2id password hash. '''
return pwhash.argon2id.str(
(config.config['secret'] + salt + password).encode('utf8')
).decode('utf8'), 3
def get_sha256_legacy_password_hash(salt: str, password: str) -> Tuple[str, int]:
''' Retrieve old-style sha256 password hash. '''
digest = hashlib.sha256()
digest.update(config.config['secret'].encode('utf8'))
digest.update(salt.encode('utf8'))
digest.update(password.encode('utf8'))
return digest.hexdigest(), 2
def get_sha1_legacy_password_hash(salt: str, password: str) -> Tuple[str, int]:
''' Retrieve old-style sha1 password hash. '''
digest = hashlib.sha1()
digest.update(b'1A2/$_4xVa')
digest.update(salt.encode('utf8'))
digest.update(password.encode('utf8'))
return digest.hexdigest(), 1
def create_password() -> str:
alphabet = {
'c': list('bcdfghijklmnpqrstvwxyz'),
'v': list('aeiou'),
'n': list('0123456789'),
}
pattern = 'cvcvnncvcv'
return ''.join(random.choice(alphabet[l]) for l in list(pattern))
def is_valid_password(user: model.User, password: str) -> bool:
assert user
salt, valid_hash = user.password_salt, user.password_hash
try:
return pwhash.verify(
user.password_hash.encode('utf8'),
(config.config['secret'] + salt + password).encode('utf8'))
except InvalidkeyError:
possible_hashes = [
get_sha256_legacy_password_hash(salt, password)[0],
get_sha1_legacy_password_hash(salt, password)[0]
]
if valid_hash in possible_hashes:
# Convert the user password hash to the new hash
new_hash, revision = get_password_hash(salt, password)
user.password_hash = new_hash
user.password_revision = revision
db.session.commit()
return True
return False
def has_privilege(user: model.User, privilege_name: str) -> bool:
assert user
all_ranks = list(RANK_MAP.keys())
assert privilege_name in config.config['privileges']
assert user.rank in all_ranks
minimal_rank = util.flip(RANK_MAP)[
config.config['privileges'][privilege_name]]
good_ranks = all_ranks[all_ranks.index(minimal_rank):]
return user.rank in good_ranks
def verify_privilege(user: model.User, privilege_name: str) -> None:
assert user
if not has_privilege(user, privilege_name):
raise errors.AuthError('Insufficient privileges to do this.')
def generate_authentication_token(user: model.User) -> str:
''' Generate nonguessable challenge (e.g. links in password reminder). '''
assert user
digest = hashlib.md5()
digest.update(config.config['secret'].encode('utf8'))
digest.update(user.password_salt.encode('utf8'))
return digest.hexdigest()