Files
controller-wisetopdataserver/python/biopro/file/user/password.py
T
2021-12-20 14:52:55 +08:00

358 lines
9.0 KiB
Python

# Copyright (c) 2020. BioPro. Scientific.
"""
Command line
------------
example password hash and verify.
Usage ::
env PYTHONPATH=RaspBerryPi3/python python -m biopro.file.user.password PASSWORD [HASH]
Reference
---------
https://www.vitoshacademy.com/hashing-passwords-in-python/
"""
import abc
import binascii
import hashlib
from pathlib import Path
from typing import List, Dict, Iterable, Optional
try:
import secrets
except ImportError:
from . import _secrets as secrets
from biopro.text import *
from biopro.util.lock import *
from biopro.util.text import part_suffix
# https://en.wikipedia.org/wiki/List_of_the_most_common_passwords
_COMMON_ = {
# number
'1234'
'123123'
'4321'
'5678'
'abcd',
# keyboard layout
'qwerty',
'azerty',
'qwerasdf',
'qazwsx',
'1q2w3e4r',
# meaning word
'admin',
'baseball',
'charlie',
'donald',
'dragon',
'football',
'iloveyou',
'master',
'monkey',
'password',
'princess',
'qwertyuiop',
'sunshine',
'superman',
'whatever',
'welcome',
# symbol
'!@#$%^&*',
# 注音
'au3a83', # 密碼
# XXX load from file?
}
def hash_password(password: str) -> str:
"""hash password.
:param password: user password input
:return: hashed password
"""
salt = secrets.token_bytes(64)
pw_hash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt, 100000)
return binascii.hexlify(salt + pw_hash).decode('ascii')
def verify_password(stored: str, provided: str) -> bool:
"""
:param stored: hashed password
:param provided: password
:return:
"""
stored = binascii.unhexlify(stored.encode('ascii'))
salt = stored[:64]
pw0_hash = stored[64:]
pw1_hash = hashlib.pbkdf2_hmac('sha512', provided.encode('utf-8'), salt, 100000)
return pw0_hash == pw1_hash
class PasswordAPI(metaclass=abc.ABCMeta):
"""user account manager API"""
@property
@abc.abstractmethod
def password_file(self) -> Path:
"""password storage file path"""
pass
@abc.abstractmethod
def user_list(self) -> List[str]:
"""user stored in file"""
pass
@abc.abstractmethod
def user_hash(self, user: str) -> str:
"""get user hashed password"""
pass
@abc.abstractmethod
def user_verify(self, user: str, password: str) -> bool:
"""verify user password.
This method don't raise any error, no matter the incorrect password or un-existed user.
:param user:
:param password:
:return:
"""
pass
@abc.abstractmethod
def user_add(self, user: str, password: str):
"""add user"""
pass
@abc.abstractmethod
def check_password_strength(self, password: str) -> Optional[str]:
"""check the password is strength enough.
:param password:
:return: suggestion message, None when pass
"""
pass
@abc.abstractmethod
def user_change(self, user: str, password: str, new_password: str):
"""change user password.
:param user:
:param password: old password, or temporary password generated by :func:`user_forget`
:param new_password: new password.
:return:
"""
pass
@abc.abstractmethod
def user_forget(self, user: str) -> str:
"""forget user password.
This method would generate short, temporary password for user, which they can use it to
change the password by method :func:`user_change`.
The temporary password will be kept by manager until the owned user change their password,
or controller server restart the process.
The temporary password aren't stored into password file.
:param user:
:return: new temp password
"""
pass
@abc.abstractmethod
def user_del(self, user: str, password: str):
"""delete user"""
pass
class PasswordManager(PasswordAPI, Synchronized, object):
"""user account manager"""
__slots__ = '__filepath', '__password', '__forget'
def __init__(self, filepath: Path):
Synchronized.__init__(self)
self.__filepath = filepath
self.__password = self.__load_file(self.__filepath)
self.__forget: Dict[str, str] = {}
def __init_subclass__(cls) -> None:
# no subclass
raise RuntimeError()
@staticmethod
def __load_file(filepath: Path) -> Dict[str, str]:
if not filepath.exists():
return {}
if not filepath.is_file():
raise IsADirectoryError(str(filepath))
ret = {}
with filepath.open('r') as f:
for line in f:
p, u = part_suffix(line.strip(), ' ')
ret[u] = p
return ret
@staticmethod
def __sync_file(filepath: Path, data: Dict[str, str]):
d: Path = filepath.parent
if not d.exists():
d.mkdir(parents=True)
with filepath.open('w') as f:
for u in sorted(data.keys()):
p = data[u]
print(p, u, file=f)
@property
def password_file(self) -> Path:
return self.__filepath
def user_list(self) -> List[str]:
return list(self.__password.keys())
def user_hash(self, user: str) -> str:
try:
return self.__password[user]
except KeyError:
pass
raise ValueError(USER_NOT_FOUND, user)
def user_verify(self, user: str, password: str, strict=True) -> bool:
"""verify user password.
This method don't raise any error, no matter the incorrect password or un-existed user.
:param user:
:param password:
:param strict: do not verify the temp password if their forget.
:return:
"""
try:
if verify_password(self.__password[user], password):
return True
if not strict and verify_password(self.__forget[user], password):
return True
return False
except KeyError:
return False
@synchronized
def user_add(self, user: str, password: str):
if user in self.__password:
raise ValueError(USER_HAS_BEEN_USED, user)
message = self.check_password_strength(password, user)
if message is not None:
raise ValueError(PASSWORD_TOO_WEAK, message)
self.__password[user] = hash_password(password)
self.__sync_file(self.__filepath, self.__password)
def check_password_strength(self, password: str, user_name: str = None) -> Optional[str]:
password = password.lower()
if password == user_name.lower():
return 'same as user name'
if len(password) < 8:
return 'password too short'
if len(set(password)) < 4:
return 'password too simple'
if all([c in '0123456789' for c in password]):
return 'password should be contain alphabet characters'
if password in _COMMON_:
return 'password too common'
if any([c in password for c in _COMMON_]):
return 'password too common'
return None
@synchronized
def user_change(self, user: str, password: str, new_password: str):
# raise error if user not existed.
self.user_hash(user)
if not self.user_verify(user, password, strict=False):
raise RuntimeError(PASSWORD_NOT_CORRECT, user)
message = self.check_password_strength(new_password, user)
if message is not None:
raise ValueError(PASSWORD_TOO_WEAK, message)
self.__password[user] = hash_password(new_password)
self.__sync_file(self.__filepath, self.__password)
# remove user if their forget the password
try:
del self.__forget[user]
except KeyError:
pass
def user_forget(self, user: str) -> str:
new_password = secrets.token_urlsafe(8)
self.__forget[user] = hash_password(new_password)
return new_password
@synchronized
def user_del(self, user: str, password: str):
# raise error if user not existed.
self.user_hash(user)
if not self.user_verify(user, password, strict=False):
raise RuntimeError(PASSWORD_NOT_CORRECT, user)
del self.__password[user]
self.__sync_file(self.__filepath, self.__password)
# remove user if their forget the password
try:
del self.__forget[user]
except KeyError:
pass
def __dir__(self) -> Iterable[str]:
raise RuntimeError()
def __str__(self):
raise RuntimeError()
def __repr__(self):
raise RuntimeError()
if __name__ == '__main__':
import sys
if len(sys.argv) == 1:
print(sys.argv[0], 'PASSWORD', '[HASH]')
elif len(sys.argv) == 2:
print(hash_password(sys.argv[1]))
elif len(sys.argv) == 3:
print(verify_password(sys.argv[2], sys.argv[1]))
else:
raise ValueError('unknown argument : ' + sys.argv[3])