forked from platypush/platypush
[#397] Replaced bcrypt dependency with native hashlib logic.
Closes: #397
This commit is contained in:
parent
a5826892dd
commit
901338e228
9 changed files with 106 additions and 21 deletions
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
python
|
python
|
||||||
python-alembic
|
python-alembic
|
||||||
python-bcrypt
|
|
||||||
python-croniter
|
python-croniter
|
||||||
python-dateutil
|
python-dateutil
|
||||||
python-docutils
|
python-docutils
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
###
|
###
|
||||||
|
|
||||||
alembic
|
alembic
|
||||||
bcrypt
|
|
||||||
croniter
|
croniter
|
||||||
docutils
|
docutils
|
||||||
flask
|
flask
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -67,7 +67,6 @@ setup(
|
||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'alembic',
|
'alembic',
|
||||||
'bcrypt',
|
|
||||||
'croniter',
|
'croniter',
|
||||||
'docutils',
|
'docutils',
|
||||||
'flask',
|
'flask',
|
||||||
|
|
Loading…
Reference in a new issue