[#397] Replaced bcrypt dependency with native hashlib logic.

Closes: #397
This commit is contained in:
Fabio Manganiello 2024-05-05 21:38:27 +02:00
parent a5826892dd
commit 901338e228
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
9 changed files with 106 additions and 21 deletions

View file

@ -47,7 +47,7 @@
<DropdownItem text="Change Password" :disabled="commandRunning" icon-class="fa fa-key" <DropdownItem text="Change Password" :disabled="commandRunning" icon-class="fa fa-key"
@click="showChangePasswordModal(user)" /> @click="showChangePasswordModal(user)" />
<DropdownItem text="Delete User" :disabled="commandRunning" icon-class="fa fa-trash" <DropdownItem text="Delete User" :disabled="commandRunning" icon-class="fa fa-trash"
@click="$refs.deleteUserDialog.show()" /> @click="selectedUser = user.username; $refs.deleteUserDialog.show()" />
</Dropdown> </Dropdown>
</div> </div>
</li> </li>

View file

@ -1,7 +1,6 @@
python3 python3
py3-pip py3-pip
py3-alembic py3-alembic
py3-bcrypt
py3-dateutil py3-dateutil
py3-docutils py3-docutils
py3-flask py3-flask

View file

@ -1,6 +1,5 @@
python python
python-alembic python-alembic
python-bcrypt
python-croniter python-croniter
python-dateutil python-dateutil
python-docutils python-docutils

View file

@ -1,7 +1,6 @@
python3 python3
python3-pip python3-pip
python3-alembic python3-alembic
python3-bcrypt
python3-croniter python3-croniter
python3-dateutil python3-dateutil
python3-docutils python3-docutils

View file

@ -1,7 +1,6 @@
python python
python-pip python-pip
python-alembic python-alembic
python-bcrypt
python-croniter python-croniter
python-dateutil python-dateutil
python-docutils python-docutils

View file

@ -0,0 +1,45 @@
"""Added password hashing parameters to user table
Revision ID: 0876439530cb
Revises: c39ac404119b
Create Date: 2024-05-05 20:57:02.820575
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0876439530cb'
down_revision = 'c39ac404119b'
branch_labels = None
depends_on = None
def upgrade() -> None:
conn = op.get_bind()
metadata = sa.MetaData()
metadata.reflect(bind=conn)
user_table = metadata.tables.get('user')
if user_table is None:
print('The table `user` does not exist, skipping migration')
return
op.add_column('user', sa.Column('password_salt', sa.String(), nullable=True))
op.add_column('user', sa.Column('hmac_iterations', sa.Integer(), nullable=True))
def downgrade() -> None:
conn = op.get_bind()
metadata = sa.MetaData()
metadata.reflect(bind=conn)
user_table = metadata.tables.get('user')
if user_table is None:
print('The table `user` does not exist, skipping migration')
return
op.drop_column('user', 'password_salt')
op.drop_column('user', 'hmac_iterations')

View file

@ -2,11 +2,11 @@ import base64
import datetime import datetime
import hashlib import hashlib
import json import json
import os
import random import random
import time import time
from typing import Optional, Dict from typing import Optional, Dict
import bcrypt
import rsa import rsa
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
@ -58,7 +58,7 @@ class UserManager:
with self._get_session() as session: with self._get_session() as session:
return session.query(User).all() return session.query(User).all()
def create_user(self, username, password, **kwargs): def create_user(self, username: str, password: str, **kwargs):
if not username: if not username:
raise ValueError('Invalid or empty username') raise ValueError('Invalid or empty username')
if not password: if not password:
@ -67,11 +67,17 @@ class UserManager:
with self._get_session(locked=True) as session: with self._get_session(locked=True) as session:
user = self._get_user(session, username) user = self._get_user(session, username)
if user: if user:
raise NameError('The user {} already exists'.format(username)) raise NameError(f'The user {username} already exists')
password_salt = os.urandom(16)
hmac_iterations = 100_000
record = User( record = User(
username=username, username=username,
password=self._encrypt_password(password), password=self._encrypt_password(
password, password_salt, hmac_iterations
),
password_salt=password_salt.hex(),
hmac_iterations=hmac_iterations,
created_at=datetime.datetime.utcnow(), created_at=datetime.datetime.utcnow(),
**kwargs, **kwargs,
) )
@ -88,7 +94,12 @@ class UserManager:
return False return False
user = self._get_user(session, username) user = self._get_user(session, username)
user.password = self._encrypt_password(new_password) user.password_salt = user.password_salt or os.urandom(16).hex()
user.hmac_iterations = user.hmac_iterations or 100_000
salt = bytes.fromhex(user.password_salt)
user.password = self._encrypt_password(
new_password, salt, user.hmac_iterations
)
session.commit() session.commit()
return True return True
@ -117,7 +128,7 @@ class UserManager:
with self._get_session(locked=True) as session: with self._get_session(locked=True) as session:
user = self._get_user(session, username) user = self._get_user(session, username)
if not user: if not user:
raise NameError('No such user: {}'.format(username)) raise NameError(f'No such user: {username}')
user_sessions = ( user_sessions = (
session.query(UserSession).filter_by(user_id=user.user_id).all() session.query(UserSession).filter_by(user_id=user.user_id).all()
@ -172,15 +183,43 @@ class UserManager:
def _get_user(session, username): def _get_user(session, username):
return session.query(User).filter_by(username=username).first() return session.query(User).filter_by(username=username).first()
@staticmethod @classmethod
def _encrypt_password(pwd): def _encrypt_password(
if isinstance(pwd, str): cls, pwd: str, salt: Optional[bytes] = None, iterations: Optional[int] = None
pwd = pwd.encode() ) -> str:
return bcrypt.hashpw(pwd, bcrypt.gensalt(12)).decode() # Legacy password check that uses bcrypt if no salt and iterations are provided
# See https://git.platypush.tech/platypush/platypush/issues/397
if not (salt and iterations):
import bcrypt
return bcrypt.hashpw(pwd.encode(), bcrypt.gensalt(12)).decode()
return hashlib.pbkdf2_hmac('sha256', pwd.encode(), salt, iterations).hex()
@classmethod @classmethod
def _check_password(cls, pwd, hashed_pwd): def _check_password(
return bcrypt.checkpw(cls._to_bytes(pwd), cls._to_bytes(hashed_pwd)) cls,
pwd: str,
hashed_pwd: str,
salt: Optional[bytes] = None,
iterations: Optional[int] = None,
) -> bool:
# Legacy password check that uses bcrypt if no salt and iterations are provided
# See https://git.platypush.tech/platypush/platypush/issues/397
if not (salt and iterations):
import bcrypt
return bcrypt.checkpw(pwd.encode(), hashed_pwd.encode())
return (
hashlib.pbkdf2_hmac(
'sha256',
pwd.encode(),
salt,
iterations,
).hex()
== hashed_pwd
)
@staticmethod @staticmethod
def _to_bytes(data) -> bytes: def _to_bytes(data) -> bytes:
@ -289,7 +328,12 @@ class UserManager:
if not user: if not user:
return None return None
if not self._check_password(password, user.password): if not self._check_password(
password,
user.password,
bytes.fromhex(user.password_salt) if user.password_salt else None,
user.hmac_iterations,
):
return None return None
return user return user
@ -304,6 +348,8 @@ class User(Base):
user_id = Column(Integer, primary_key=True) user_id = Column(Integer, primary_key=True)
username = Column(String, unique=True, nullable=False) username = Column(String, unique=True, nullable=False)
password = Column(String) password = Column(String)
password_salt = Column(String)
hmac_iterations = Column(Integer)
created_at = Column(DateTime) created_at = Column(DateTime)

View file

@ -3,7 +3,6 @@
### ###
alembic alembic
bcrypt
croniter croniter
docutils docutils
flask flask

View file

@ -67,7 +67,6 @@ setup(
], ],
install_requires=[ install_requires=[
'alembic', 'alembic',
'bcrypt',
'croniter', 'croniter',
'docutils', 'docutils',
'flask', 'flask',