6 Commits

Author SHA1 Message Date
a1bed9014d Changed the way the master password hashing works
Switched from Bcrypt to Scrypt for master password hashing
Changed models to use new sizes for hashes and salts, doubled the size of enc_login and enc_passwd for accounts
Created new function to check master password validity
Increased salt sizes for accounts and master passwords
Removed bcrypt from requirements
2022-11-07 16:30:45 +03:00
66ab13b45d Changed handlers.get_accounts to use a guard clause, added comment to clarify, why we are putting account in backticks 2022-11-07 16:29:37 +03:00
da42d7ad1d Changed comments in cryptograpy.other_accounts to be more precise 2022-11-07 16:29:31 +03:00
570f15001e switched mariadb connector to pymysql 2022-11-04 02:17:17 +03:00
b4bf9fbf41 Changed mariadb.Connection to Engine in __init__ of bot 2022-11-04 02:16:47 +03:00
042ca9312e Cleaned up code in handlers
Renamed variables in _set_master_pass2 for consistency
Added a few missing returns in guard clauses
Added file size limit for importing account to 100 MB
2022-11-04 01:20:25 +03:00
8 changed files with 76 additions and 62 deletions

View File

@ -13,9 +13,6 @@ RUN adduser -u 1000 --disabled-password --gecos "" appuser && chown -R appuser /
# Install deps
RUN apt update && apt full-upgrade -y
RUN apt install curl gcc g++ -y
RUN curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash
RUN apt install libmariadb3 libmariadb-dev -y
# Install pip requirements
COPY requirements.txt .

View File

@ -1,6 +1,5 @@
bcrypt
cryptography
mariadb
pymysql
python-dotenv
pyTelegramBotAPI
sqlmodel

View File

@ -1,6 +1,6 @@
import functools
import mariadb
from sqlalchemy.future import Engine
import telebot
from . import handlers, utils
@ -8,7 +8,7 @@ from . import handlers, utils
__all__ = ["handlers", "utils"]
def create_bot(token: str, engine: mariadb.Connection) -> telebot.TeleBot:
def create_bot(token: str, engine: Engine) -> telebot.TeleBot:
bot = telebot.TeleBot(token)
bot.register_message_handler(
functools.partial(handlers.set_master_password, bot, engine),

View File

@ -26,17 +26,19 @@ def get_accounts(
) -> None:
base_handler(bot, mes)
accounts = database.get.get_accounts(engine, mes.from_user.id)
if accounts:
accounts = [f"`{account}`" for account in accounts]
return send_tmp_message(
bot,
mes.chat.id,
"Ваши аккаунты:\n"
+ "\n".join(accounts)
+ "\nНажмите на название, чтобы скопировать",
30,
)
send_tmp_message(bot, mes.chat.id, "У вас нет аккаунтов")
if not accounts:
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(
bot,
mes.chat.id,
"Ваши аккаунты:\n"
+ "\n".join(accounts)
+ "\nНажмите на название, чтобы скопировать",
30,
)
def delete_all(
@ -83,8 +85,8 @@ def _set_master_pass2(
if text == "/cancel":
return send_tmp_message(bot, mes.chat.id, "Успешная отмена")
hash_, salt = cryptography.master_pass.encrypt_master_pass(text)
database.add.add_master_pass(engine, mes.from_user.id, salt, hash_)
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, "Успех")
del mes, text
gc.collect()
@ -217,7 +219,7 @@ def _add_account5(
return send_tmp_message(bot, mes.chat.id, "Успешная отмена")
salt, hash_ = database.get.get_master_pass(engine, mes.from_user.id)
if cryptography.master_pass.encrypt_master_pass(text, salt) != hash_:
if not cryptography.master_pass.check_master_pass(text, hash_, salt):
return send_tmp_message(bot, mes.chat.id, "Не подходит главный пароль")
name, login, passwd = data["name"], data["login"], data["passwd"]
@ -242,6 +244,11 @@ def _add_account5(
def get_account(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None:
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, "Нет мастер пароля")
bot.register_next_step_handler(
mes, functools.partial(_get_account2, bot, engine, bot_mes)
)
@ -272,13 +279,9 @@ def _get_account3(
if text == "/cancel":
return send_tmp_message(bot, 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, "Нет мастер пароля")
master_salt, hash_pass = database.get.get_master_pass(engine, mes.from_user.id)
master_salt, hash_pass = master_pass
if cryptography.master_pass.encrypt_master_pass(text, master_salt) != hash_pass:
if not cryptography.master_pass.check_master_pass(text, hash_pass, master_salt):
return send_tmp_message(bot, mes.chat.id, "Не подходит мастер пароль")
salt, enc_login, enc_pass = database.get.get_account_info(
@ -300,6 +303,11 @@ def _get_account3(
def delete_account(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None:
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, "Нет мастер пароля")
bot_mes = bot.send_message(
mes.chat.id, "Отправьте название аккаунта, который вы хотите удалить"
)
@ -370,7 +378,7 @@ def _export2(
return send_tmp_message(bot, mes.chat.id, "Успешная отмена")
master_salt, hash_pass = database.get.get_master_pass(engine, mes.from_user.id)
if cryptography.master_pass.encrypt_master_pass(text, master_salt) != hash_pass:
if not cryptography.master_pass.check_master_pass(text, hash_pass, master_salt):
return send_tmp_message(bot, mes.chat.id, "Не подходит мастер пароль")
accounts = get_all_accounts(engine, mes.from_user.id, text)
@ -407,14 +415,16 @@ def _import2(
return send_tmp_message(bot, mes.chat.id, "Успешная отмена")
if mes.document is None:
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, "Файл слишком большой")
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:
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(
@ -435,7 +445,7 @@ def _import3(
return send_tmp_message(bot, mes.chat.id, "Успешная отмена")
master_salt, hash_pass = database.get.get_master_pass(engine, mes.from_user.id)
if cryptography.master_pass.encrypt_master_pass(text, master_salt) != hash_pass:
if not cryptography.master_pass.check_master_pass(text, hash_pass, master_salt):
return send_tmp_message(bot, mes.chat.id, "Не подходит мастер пароль")
# List of names of accounts, which failed to be added to the database or failed tests

View File

@ -1,26 +1,35 @@
from typing import overload
import os
import bcrypt
from cryptography.exceptions import InvalidKey
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
_memory_use = 2**14
@overload
def encrypt_master_pass(passwd: str, salt: bytes) -> bytes:
...
def _get_kdf(salt: bytes) -> Scrypt:
kdf = Scrypt(
salt=salt,
length=128,
n=_memory_use,
r=8,
p=1,
)
return kdf
@overload
def encrypt_master_pass(passwd: str) -> tuple[bytes, bytes]:
...
def encrypt_master_pass(
passwd: str, salt: bytes | None = None
) -> tuple[bytes, bytes] | bytes:
"""Hashes master password and return tuple of hashed password and salt"""
if salt is None:
salt = bcrypt.gensalt()
gened_salt = True
salt = os.urandom(64)
kdf = _get_kdf(salt)
return kdf.derive(passwd.encode("utf-8")), salt
def check_master_pass(passwd: str, enc_pass: bytes, salt: bytes) -> bool:
"""Checks if the master password is correct"""
kdf = _get_kdf(salt)
try:
kdf.verify(passwd.encode("utf-8"), enc_pass)
except InvalidKey:
return False
else:
gened_salt = False
hashed = bcrypt.hashpw(passwd.encode("utf-8"), salt)
return (hashed, salt) if gened_salt else hashed
return True

View File

@ -1,6 +1,5 @@
import base64
import bcrypt
import os
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
@ -23,9 +22,9 @@ def _generate_key(salt: bytes, master_pass: bytes) -> bytes:
def encrypt_account_info(
login: str, passwd: str, master_pass: bytes
) -> tuple[bytes, bytes, bytes]:
"""Encrypts login and password of a user using hash of their master password as a key.
Returns a tuple of encrypted login password and salt"""
salt = bcrypt.gensalt()
"""Encrypts login and password of a user using their master password as a key.
Returns a tuple of encrypted login, password and salt"""
salt = os.urandom(64)
key = _generate_key(salt, master_pass)
f = Fernet(key)
enc_login = f.encrypt(login.encode("utf-8"))
@ -36,6 +35,8 @@ def encrypt_account_info(
def decrypt_account_info(
enc_login: bytes, enc_pass: bytes, master_pass: bytes, salt: bytes
) -> tuple[str, str]:
"""Decrypts login and password using their master password as a key.
Returns a tuple of decrypted login and password"""
key = _generate_key(salt, master_pass)
f = Fernet(key)
login_bytes = f.decrypt(enc_login)

View File

@ -8,10 +8,10 @@ class MasterPass(sqlmodel.SQLModel, table=True):
id: Optional[int] = sqlmodel.Field(primary_key=True)
user_id: int = sqlmodel.Field(nullable=False, index=True, unique=True)
salt: bytes = sqlmodel.Field(
sa_column=sqlmodel.Column(sqlmodel.VARBINARY(255), nullable=False)
sa_column=sqlmodel.Column(sqlmodel.BINARY(64), nullable=False)
)
passwd: bytes = sqlmodel.Field(
sa_column=sqlmodel.Column(sqlmodel.VARBINARY(255), nullable=False)
sa_column=sqlmodel.Column(sqlmodel.BINARY(128), nullable=False)
)
@ -22,11 +22,11 @@ class Account(sqlmodel.SQLModel, table=True):
user_id: int = sqlmodel.Field(nullable=False, index=True)
name: str = sqlmodel.Field(nullable=False, index=True, max_length=255)
salt: bytes = sqlmodel.Field(
sa_column=sqlmodel.Column(sqlmodel.VARBINARY(255), nullable=False)
sa_column=sqlmodel.Column(sqlmodel.BINARY(64), nullable=False)
)
enc_login: bytes = sqlmodel.Field(
sa_column=sqlmodel.Column(sqlmodel.VARBINARY(255), nullable=False)
sa_column=sqlmodel.Column(sqlmodel.VARBINARY(500), nullable=False)
)
enc_pass: bytes = sqlmodel.Field(
sa_column=sqlmodel.Column(sqlmodel.VARBINARY(255), nullable=False)
sa_column=sqlmodel.Column(sqlmodel.VARBINARY(500), nullable=False)
)

View File

@ -5,9 +5,7 @@ from . import models
def get_engine(host: str, user: str, passwd: str, db: str) -> Engine:
engine = sqlmodel.create_engine(
f"mariadb+mariadbconnector://{user}:{passwd}@{host}/{db}"
)
engine = sqlmodel.create_engine(f"mariadb+pymysql://{user}:{passwd}@{host}/{db}")
return engine