From e9eaa085a22df63d3b281519b72b06a8d674a4bc Mon Sep 17 00:00:00 2001 From: StNicolay Date: Wed, 30 Nov 2022 16:28:37 +0300 Subject: [PATCH] Moved utils into src directory, moved most of the functions from it into separate files --- src/__init__.py | 18 ++++- src/account_checks.py | 42 ++++++++++ src/account_parsing.py | 37 +++++++++ src/bot/__init__.py | 5 +- src/bot/handlers.py | 172 ++++++++++++++++++++++------------------- src/bot/utils.py | 130 ------------------------------- src/gen_password.py | 18 +++++ src/utils.py | 16 ++++ 8 files changed, 226 insertions(+), 212 deletions(-) create mode 100644 src/account_checks.py create mode 100644 src/account_parsing.py delete mode 100644 src/bot/utils.py create mode 100644 src/gen_password.py create mode 100644 src/utils.py diff --git a/src/__init__.py b/src/__init__.py index 9ea0a54..a4fd0a9 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -2,9 +2,23 @@ import os from dotenv import load_dotenv -from . import bot, cryptography, database +from . import ( + account_checks, + account_parsing, + bot, + cryptography, + database, + gen_password, +) -__all__ = ["bot", "cryptography", "database"] +__all__ = [ + "account_checks", + "account_parsing", + "bot", + "cryptography", + "database", + "gen_password", +] def main() -> None: diff --git a/src/account_checks.py b/src/account_checks.py new file mode 100644 index 0000000..7c824ad --- /dev/null +++ b/src/account_checks.py @@ -0,0 +1,42 @@ +import string + +FORBIDDEN_CHARS = frozenset("`\n") +PUNCTUATION = frozenset(string.punctuation).difference(FORBIDDEN_CHARS) + + +def _base_check(val: str, /) -> bool: + "Returns false if finds new lines or backtick (`)" + return not any(i in FORBIDDEN_CHARS for i in val) + + +def check_account_name(name: str) -> bool: + "Returns true if account name is valid" + return _base_check(name) + + +def check_login(login: str) -> bool: + "Returns true if login is valid" + return _base_check(login) + + +def check_password(passwd: str) -> bool: + "Returns true if password is valid" + return _base_check(passwd) + + +def check_account(name: str, login: str, passwd: str) -> bool: + """Runs checks for account name, login and password""" + return check_account_name(name) and check_login(login) and check_password(passwd) + + +def check_gened_password(passwd: str, /) -> bool: + """Retuns true if generated password is valid, + false otherwise. + Password is valid if there is at least one lowercase character, + uppercase character and one punctuation character""" + return ( + any(c.islower() for c in passwd) + and any(c.isupper() for c in passwd) + and any(c.isdigit() for c in passwd) + and any(c in PUNCTUATION for c in passwd) + ) diff --git a/src/account_parsing.py b/src/account_parsing.py new file mode 100644 index 0000000..712890c --- /dev/null +++ b/src/account_parsing.py @@ -0,0 +1,37 @@ +import io +from typing import Iterator, Self, Type + +import pydantic + + +class _Account(pydantic.BaseModel): + name: str + login: str + passwd: str + + @classmethod + def from_tuple(cls: Type[Self], tuple_: tuple[str, str, str]) -> Self: + return cls(name=tuple_[0], login=tuple_[1], passwd=tuple_[2]) + + def as_tuple(self: Self) -> tuple[str, str, str]: + return (self.name, self.login, self.passwd) + + +class _Accounts(pydantic.BaseModel): + accounts: list[_Account] = pydantic.Field(default_factory=list) + + +def _accounts_list_to_json(accounts: Iterator[tuple[str, str, str]]) -> str: + accounts = _Accounts(accounts=[_Account.from_tuple(i) for i in accounts]) + return accounts.json() + + +def json_to_accounts(json_: str) -> list[tuple[str, str, str]]: + accounts = _Accounts.parse_raw(json_) + return [i.as_tuple() for i in accounts.accounts] + + +def accounts_to_json(accounts: Iterator[tuple[str, str, str]]) -> io.StringIO: + file = io.StringIO(_accounts_list_to_json(accounts)) + file.name = "passwords.json" + return file diff --git a/src/bot/__init__.py b/src/bot/__init__.py index 0fb9325..ef8dedd 100644 --- a/src/bot/__init__.py +++ b/src/bot/__init__.py @@ -1,9 +1,10 @@ import functools -from sqlalchemy.future import Engine import telebot +from sqlalchemy.future import Engine -from . import handlers, utils +from .. import utils +from . import handlers __all__ = ["handlers", "utils"] diff --git a/src/bot/handlers.py b/src/bot/handlers.py index 5c17310..53773ed 100644 --- a/src/bot/handlers.py +++ b/src/bot/handlers.py @@ -6,31 +6,47 @@ import telebot from sqlalchemy.future import Engine from .. import cryptography, database -from .utils import ( - accounts_to_json, - base_handler, +from ..account_checks import ( check_account, check_account_name, check_login, - check_passwd, - gen_passwd, - get_all_accounts, - json_to_accounts, - send_tmp_message, + check_password, ) +from ..account_parsing import accounts_to_json, json_to_accounts +from ..gen_password import gen_password +from ..utils import get_all_accounts Message = telebot.types.Message +def _send_tmp_message( + bot: telebot.TeleBot, + chat_id: telebot.types.Message, + text: str, + timeout: int = 5, +) -> None: + bot_mes = bot.send_message(chat_id, text, parse_mode="MarkdownV2") + time.sleep(timeout) + bot.delete_message(chat_id, bot_mes.id) + + +def _base_handler( + bot: telebot.TeleBot, mes: Message, prev_mes: Message | None = None +) -> None: + bot.delete_message(mes.chat.id, mes.id) + if prev_mes is not None: + bot.delete_message(prev_mes.chat.id, prev_mes.id) + + def get_accounts(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: - base_handler(bot, mes) + _base_handler(bot, mes) accounts = database.get.get_accounts(engine, mes.from_user.id) if not accounts: - return send_tmp_message(bot, mes.chat.id, "У вас нет аккаунтов") + return _send_tmp_message(bot, mes.chat.id, "У вас нет аккаунтов") # Make accounts copyable and escape special chars accounts = [f"`{account}`" for account in accounts] - send_tmp_message( + _send_tmp_message( bot, mes.chat.id, "Ваши аккаунты:\n" @@ -41,7 +57,7 @@ def get_accounts(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: def delete_all(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: - base_handler(bot, mes) + _base_handler(bot, mes) bot_mes = bot.send_message( mes.chat.id, "Вы действительно хотите удалить все ваши аккаунты? Это действие нельзя отменить. " @@ -55,20 +71,20 @@ def delete_all(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: def _delete_all( bot: telebot.TeleBot, engine: Engine, prev_mes: Message, mes: Message ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "YES": database.delete.purge_accounts(engine, mes.from_user.id) database.delete.delete_master_pass(engine, mes.from_user.id) - send_tmp_message(bot, mes.chat.id, "Всё успешно удалено", timeout=10) + _send_tmp_message(bot, mes.chat.id, "Всё успешно удалено", timeout=10) else: - send_tmp_message(bot, mes.chat.id, "Вы отправили не YES, ничего не удалено") + _send_tmp_message(bot, mes.chat.id, "Вы отправили не YES, ничего не удалено") def set_master_password(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: - base_handler(bot, mes, None) + _base_handler(bot, mes, None) if database.get.get_master_pass(engine, mes.from_user.id) is not None: - return send_tmp_message(bot, mes.chat.id, "Мастер пароль уже существует") + return _send_tmp_message(bot, mes.chat.id, "Мастер пароль уже существует") bot_mes = bot.send_message(mes.chat.id, "Отправьте мастер пароль") bot.register_next_step_handler( mes, functools.partial(_set_master_pass2, bot, engine, bot_mes) @@ -78,22 +94,22 @@ def set_master_password(bot: telebot.TeleBot, engine: Engine, mes: Message) -> N def _set_master_pass2( bot: telebot.TeleBot, engine: Engine, prev_mes: Message, mes: Message ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") hash_pass, master_salt = cryptography.master_pass.encrypt_master_pass(text) database.add.add_master_pass(engine, mes.from_user.id, master_salt, hash_pass) - send_tmp_message(bot, mes.chat.id, "Успех") + _send_tmp_message(bot, mes.chat.id, "Успех") del mes, text gc.collect() def reset_master_pass(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: - base_handler(bot, mes) + _base_handler(bot, mes) if database.get.get_master_pass(engine, mes.from_user.id) is None: - return send_tmp_message(bot, mes.chat.id, "Мастер пароль не задан") + return _send_tmp_message(bot, mes.chat.id, "Мастер пароль не задан") bot_mes = bot.send_message( mes.chat.id, "Отправьте новый мастер пароль, осторожно, все текущие аккаунты " @@ -107,15 +123,15 @@ def reset_master_pass(bot: telebot.TeleBot, engine: Engine, mes: Message) -> Non def _reset_master_pass2( bot: telebot.TeleBot, engine: Engine, prev_mes: Message, mes: Message ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") hash_, salt = cryptography.master_pass.encrypt_master_pass(text) database.delete.purge_accounts(engine, mes.from_user.id) database.change.change_master_pass(engine, mes.from_user.id, salt, hash_) - send_tmp_message( + _send_tmp_message( bot, mes.chat.id, "Все ваши аккаунты удалены, а мастер пароль изменён" ) del mes, text @@ -123,11 +139,11 @@ def _reset_master_pass2( def add_account(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: - base_handler(bot, mes) + _base_handler(bot, mes) master_password_from_db = database.get.get_master_pass(engine, mes.from_user.id) if master_password_from_db is None: - return send_tmp_message(bot, mes.chat.id, "Нет мастер пароля") + return _send_tmp_message(bot, mes.chat.id, "Нет мастер пароля") bot_mes = bot.send_message(mes.chat.id, "Отправьте название аккаунта") @@ -139,15 +155,15 @@ def add_account(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: def _add_account2( bot: telebot.TeleBot, engine: Engine, prev_mes: Message, mes: Message ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") if not check_account_name(text): - return send_tmp_message(bot, mes.chat.id, "Не корректное название аккаунта") + return _send_tmp_message(bot, mes.chat.id, "Не корректное название аккаунта") if text in database.get.get_accounts(engine, mes.from_user.id): - return send_tmp_message( + return _send_tmp_message( bot, mes.chat.id, "Аккаунт с таким именем уже существует" ) @@ -166,12 +182,12 @@ def _add_account3( data: dict[str, str], mes: Message, ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") if not check_login(text): - return send_tmp_message(bot, mes.chat.id, "Не корректный логин") + return _send_tmp_message(bot, mes.chat.id, "Не корректный логин") data["login"] = text @@ -189,12 +205,12 @@ def _add_account4( data: dict[str, str], mes: Message, ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") - if not check_passwd(text): - return send_tmp_message(bot, mes.chat.id, "Не корректный пароль") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") + if not check_password(text): + return _send_tmp_message(bot, mes.chat.id, "Не корректный пароль") data["passwd"] = text @@ -212,14 +228,14 @@ def _add_account5( data: dict[str, str], mes: Message, ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") salt, hash_ = database.get.get_master_pass(engine, mes.from_user.id) if not cryptography.master_pass.check_master_pass(text, hash_, salt): - return send_tmp_message(bot, mes.chat.id, "Не подходит главный пароль") + return _send_tmp_message(bot, mes.chat.id, "Не подходит главный пароль") name, login, passwd = data["name"], data["login"], data["passwd"] @@ -231,7 +247,7 @@ def _add_account5( engine, mes.from_user.id, name, salt, enc_login, enc_pass ) - send_tmp_message( + _send_tmp_message( bot, mes.chat.id, "Успех" if result else "Произошла не предвиденная ошибка" ) @@ -241,12 +257,12 @@ def _add_account5( def get_account(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: - base_handler(bot, mes) + _base_handler(bot, mes) bot_mes = bot.send_message(mes.chat.id, "Отправьте название аккаунта") master_pass = database.get.get_master_pass(engine, mes.from_user.id) if master_pass is None: - return send_tmp_message(bot, mes.chat.id, "Нет мастер пароля") + return _send_tmp_message(bot, mes.chat.id, "Нет мастер пароля") bot.register_next_step_handler( mes, functools.partial(_get_account2, bot, engine, bot_mes) @@ -256,13 +272,13 @@ def get_account(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: def _get_account2( bot: telebot.TeleBot, engine: Engine, prev_mes: Message, mes: Message ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") if text not in database.get.get_accounts(engine, mes.from_user.id): - return send_tmp_message(bot, mes.chat.id, "Нет такого аккаунта") + return _send_tmp_message(bot, mes.chat.id, "Нет такого аккаунта") bot_mes = bot.send_message(mes.chat.id, "Отправьте мастер пароль") bot.register_next_step_handler( @@ -273,15 +289,15 @@ def _get_account2( def _get_account3( bot: telebot.TeleBot, engine: Engine, prev_mes: Message, name: str, mes: Message ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") master_salt, hash_pass = database.get.get_master_pass(engine, mes.from_user.id) if not cryptography.master_pass.check_master_pass(text, hash_pass, master_salt): - return send_tmp_message(bot, mes.chat.id, "Не подходит мастер пароль") + return _send_tmp_message(bot, mes.chat.id, "Не подходит мастер пароль") salt, enc_login, enc_pass = database.get.get_account_info( engine, mes.from_user.id, name @@ -289,7 +305,7 @@ def _get_account3( login, passwd = cryptography.other_accounts.decrypt_account_info( enc_login, enc_pass, text, salt ) - send_tmp_message( + _send_tmp_message( bot, mes.chat.id, f"Логин:\n`{login}`\nПароль:\n`{passwd}`\nНажмите на логин или пароль, " @@ -302,11 +318,11 @@ def _get_account3( def delete_account(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: - base_handler(bot, mes) + _base_handler(bot, mes) master_pass = database.get.get_master_pass(engine, mes.from_user.id) if master_pass is None: - return send_tmp_message(bot, mes.chat.id, "Нет мастер пароля") + return _send_tmp_message(bot, mes.chat.id, "Нет мастер пароля") bot_mes = bot.send_message( mes.chat.id, "Отправьте название аккаунта, который вы хотите удалить" @@ -320,16 +336,16 @@ def delete_account(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: def _delete_account2( bot: telebot.TeleBot, engine: Engine, prev_mes: Message, mes: Message ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") if text not in database.get.get_accounts(engine, mes.from_user.id): - return send_tmp_message(bot, mes.chat.id, "Нет такого аккаунта") + return _send_tmp_message(bot, mes.chat.id, "Нет такого аккаунта") database.delete.delete_account(engine, mes.from_user.id, text) - send_tmp_message(bot, mes.chat.id, "Аккаунт удалён") + _send_tmp_message(bot, mes.chat.id, "Аккаунт удалён") def help(bot: telebot.TeleBot, mes: Message) -> None: @@ -351,18 +367,18 @@ def help(bot: telebot.TeleBot, mes: Message) -> None: def cancel(bot: telebot.TeleBot, mes: Message) -> None: - send_tmp_message(bot, mes.chat.id, "Нет активного действия") + _send_tmp_message(bot, mes.chat.id, "Нет активного действия") def export(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: - base_handler(bot, mes) + _base_handler(bot, mes) master_password_from_db = database.get.get_master_pass(engine, mes.from_user.id) if master_password_from_db is None: - return send_tmp_message(bot, mes.chat.id, "Нет мастер пароля") + return _send_tmp_message(bot, mes.chat.id, "Нет мастер пароля") if not database.get.get_accounts(engine, mes.from_user.id): - return send_tmp_message(bot, mes.chat.id, "Нет аккаунтов") + return _send_tmp_message(bot, mes.chat.id, "Нет аккаунтов") bot_mes = bot.send_message(mes.chat.id, "Отправьте мастер пароль") @@ -374,14 +390,14 @@ def export(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: def _export2( bot: telebot.TeleBot, engine: Engine, prev_mes: Message, mes: Message ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") master_salt, hash_pass = database.get.get_master_pass(engine, mes.from_user.id) if not cryptography.master_pass.check_master_pass(text, hash_pass, master_salt): - return send_tmp_message(bot, mes.chat.id, "Не подходит мастер пароль") + return _send_tmp_message(bot, mes.chat.id, "Не подходит мастер пароль") accounts = get_all_accounts(engine, mes.from_user.id, text) json_io = accounts_to_json(accounts) @@ -394,11 +410,11 @@ def _export2( def import_accounts(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: - base_handler(bot, mes) + _base_handler(bot, mes) master_password_from_db = database.get.get_master_pass(engine, mes.from_user.id) if master_password_from_db is None: - return send_tmp_message(bot, mes.chat.id, "Нет мастер пароля") + return _send_tmp_message(bot, mes.chat.id, "Нет мастер пароля") bot_mes = bot.send_message(mes.chat.id, "Отправьте json файл") @@ -410,23 +426,23 @@ def import_accounts(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None: def _import2( bot: telebot.TeleBot, engine: Engine, prev_mes: Message, mes: Message ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) if mes.text is not None: text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") if mes.document is None: - return send_tmp_message(bot, mes.chat.id, "Вы должны отправить документ") + return _send_tmp_message(bot, mes.chat.id, "Вы должны отправить документ") if mes.document.file_size > 102_400: # If file size is bigger that 100 MB - return send_tmp_message(bot, mes.chat.id, "Файл слишком большой") + return _send_tmp_message(bot, mes.chat.id, "Файл слишком большой") file_info = bot.get_file(mes.document.file_id) downloaded_file = bot.download_file(file_info.file_path) try: accounts = json_to_accounts(downloaded_file.decode("utf-8")) except Exception: - return send_tmp_message(bot, mes.chat.id, "Ошибка во время работы с файлом") + return _send_tmp_message(bot, mes.chat.id, "Ошибка во время работы с файлом") bot_mes = bot.send_message(mes.chat.id, "Отправьте мастер пароль") bot.register_next_step_handler( @@ -441,14 +457,14 @@ def _import3( accounts: list[tuple[str, str, str]], mes: Message, ) -> None: - base_handler(bot, mes, prev_mes) + _base_handler(bot, mes, prev_mes) text = mes.text.strip() if text == "/cancel": - return send_tmp_message(bot, mes.chat.id, "Успешная отмена") + return _send_tmp_message(bot, mes.chat.id, "Успешная отмена") master_salt, hash_pass = database.get.get_master_pass(engine, mes.from_user.id) if not cryptography.master_pass.check_master_pass(text, hash_pass, master_salt): - return send_tmp_message(bot, mes.chat.id, "Не подходит мастер пароль") + return _send_tmp_message(bot, mes.chat.id, "Не подходит мастер пароль") # List of names of accounts, which failed to be added to the database or failed tests failed: list[str] = [] @@ -470,18 +486,18 @@ def _import3( mes_text = "Не удалось добавить:\n" + "\n".join(failed) else: mes_text = "Успех" - send_tmp_message(bot, mes.chat.id, mes_text, 10) + _send_tmp_message(bot, mes.chat.id, mes_text, 10) del text, mes, accounts gc.collect() def gen_password(bot: telebot.TeleBot, mes: Message) -> None: - base_handler(bot, mes) + _base_handler(bot, mes) # Generate 10 passwords and put 'em in the backticks - passwords = (f"`{gen_passwd()}`" for _ in range(10)) + passwords = (f"`{gen_password()}`" for _ in range(10)) text = ( "Пароли:\n" + "\n".join(passwords) + "\nНажмите на пароль, чтобы его скопировать" ) - send_tmp_message(bot, mes.chat.id, text, 15) + _send_tmp_message(bot, mes.chat.id, text, 15) diff --git a/src/bot/utils.py b/src/bot/utils.py deleted file mode 100644 index a7608e8..0000000 --- a/src/bot/utils.py +++ /dev/null @@ -1,130 +0,0 @@ -import io -import string -import time -from random import SystemRandom -from typing import Self, Type, Iterator - -import pydantic -import telebot -from sqlalchemy.future import Engine - -from .. import cryptography, database - - -FORBIDDEN_CHARS = frozenset("`\n") -PUNCTUATION = frozenset(string.punctuation).difference(FORBIDDEN_CHARS) -PASSWORD_CHARS = tuple( - frozenset(string.ascii_letters + string.digits).difference(FORBIDDEN_CHARS) - | PUNCTUATION -) -Message = telebot.types.Message - - -class _Account(pydantic.BaseModel): - name: str - login: str - passwd: str - - @classmethod - def from_tuple(cls: Type[Self], tuple_: tuple[str, str, str]) -> Self: - return cls(name=tuple_[0], login=tuple_[1], passwd=tuple_[2]) - - def as_tuple(self: Self) -> tuple[str, str, str]: - return (self.name, self.login, self.passwd) - - -class _Accounts(pydantic.BaseModel): - accounts: list[_Account] = pydantic.Field(default_factory=list) - - -def _accounts_list_to_json(accounts: Iterator[tuple[str, str, str]]) -> str: - accounts = _Accounts(accounts=[_Account.from_tuple(i) for i in accounts]) - return accounts.json() - - -def json_to_accounts(json_: str) -> list[tuple[str, str, str]]: - accounts = _Accounts.parse_raw(json_) - return [i.as_tuple() for i in accounts.accounts] - - -def send_tmp_message( - bot: telebot.TeleBot, - chat_id: telebot.types.Message, - text: str, - timeout: int = 5, -) -> None: - bot_mes = bot.send_message(chat_id, text, parse_mode="MarkdownV2") - time.sleep(timeout) - bot.delete_message(chat_id, bot_mes.id) - - -def base_handler( - bot: telebot.TeleBot, mes: Message, prev_mes: Message | None = None -) -> None: - bot.delete_message(mes.chat.id, mes.id) - if prev_mes is not None: - bot.delete_message(prev_mes.chat.id, prev_mes.id) - - -def get_all_accounts( - engine: Engine, user_id: int, master_pass: str -) -> Iterator[tuple[str, str, str]]: - for account in database.get.get_all_accounts(engine, user_id): - name, salt, enc_login, enc_passwd = account - login, passwd = cryptography.other_accounts.decrypt_account_info( - enc_login, enc_passwd, master_pass, salt - ) - yield (name, login, passwd) - - -def accounts_to_json(accounts: Iterator[tuple[str, str, str]]) -> io.StringIO: - file = io.StringIO(_accounts_list_to_json(accounts)) - file.name = "passwords.json" - return file - - -def _base_check(val: str, /) -> bool: - "Returns false if finds new lines or backtick (`)" - return not any(i in FORBIDDEN_CHARS for i in val) - - -def check_account_name(name: str) -> bool: - "Returns true if account name is valid" - return _base_check(name) - - -def check_login(login: str) -> bool: - "Returns true if login is valid" - return _base_check(login) - - -def check_passwd(passwd: str) -> bool: - "Returns true if password is valid" - return _base_check(passwd) - - -def check_account(name: str, login: str, passwd: str) -> bool: - """Runs checks for account name, login and password""" - return check_account_name(name) and check_login(login) and check_passwd(passwd) - - -def _check_gened_password(passwd: str, /) -> bool: - """Retuns true if generated password is valid, - false otherwise. - Password is valid if there is at least one lowercase character, - uppercase character and one punctuation character""" - return ( - any(c.islower() for c in passwd) - and any(c.isupper() for c in passwd) - and any(c.isdigit() for c in passwd) - and any(c in PUNCTUATION for c in passwd) - ) - - -def gen_passwd() -> str: - """Generates password of length 32""" - choices = SystemRandom().choices - while True: - passwd = "".join(choices(PASSWORD_CHARS, k=32)) - if _check_gened_password(passwd): - return passwd diff --git a/src/gen_password.py b/src/gen_password.py new file mode 100644 index 0000000..f826ecb --- /dev/null +++ b/src/gen_password.py @@ -0,0 +1,18 @@ +import string +from random import SystemRandom + +from .account_checks import FORBIDDEN_CHARS, PUNCTUATION, check_gened_password + +PASSWORD_CHARS = tuple( + frozenset(string.ascii_letters + string.digits).difference(FORBIDDEN_CHARS) + | PUNCTUATION +) + + +def gen_password() -> str: + """Generates password of length 32""" + choices = SystemRandom().choices + while True: + passwd = "".join(choices(PASSWORD_CHARS, k=32)) + if check_gened_password(passwd): + return passwd diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..e93f35b --- /dev/null +++ b/src/utils.py @@ -0,0 +1,16 @@ +from typing import Iterator + +from sqlalchemy.future import Engine + +from . import cryptography, database + + +def get_all_accounts( + engine: Engine, user_id: int, master_pass: str +) -> Iterator[tuple[str, str, str]]: + for account in database.get.get_all_accounts(engine, user_id): + name, salt, enc_login, enc_passwd = account + login, passwd = cryptography.other_accounts.decrypt_account_info( + enc_login, enc_passwd, master_pass, salt + ) + yield (name, login, passwd)