18 Commits
1.0 ... 1.1.1

Author SHA1 Message Date
e49f2e00eb Made pip upgrade before copying requirements.txt in Dockerfile 2022-11-18 16:38:25 +00:00
fd002e3718 Added command to update pip setuptools and install wheel in Dockerfile 2022-11-18 11:34:04 +00:00
d68a7bb6e8 Moved check of generated password into separate function 2022-11-15 15:39:38 +03:00
d5f3708c50 Refactored utils.py
Created constants FORBIDDEN_CHARS, PUNCTUATION, PASSWORD_CHARS
Removed pipe from forbidden chars
Moved Message type alias to the top of the file
Optimized _base_check and made its parameter positional only
Changed gen_password to use said constants
2022-11-15 15:17:36 +03:00
cc13b35282 Made class Account private in utils.py 2022-11-14 16:58:34 +03:00
3802943225 Changed telebot.types.Message to Message in handlers for old functions 2022-11-14 16:58:34 +03:00
309fb2108b Removed unnessesary import from src.__init__ 2022-11-14 16:58:34 +03:00
9d5c52bebe optimized password generation function 2022-11-14 16:58:26 +03:00
a61deca6fa Removed unnecessary comments and type hints in src.__init__ 2022-11-13 19:49:21 +03:00
18abdecb74 Increased time untill deletion for /gen_password to 15 secs 2022-11-13 18:53:11 +03:00
46a1fe59b2 Documented /gen_password command 2022-11-13 18:51:23 +03:00
9ee51dc19f Added /gen_password command 2022-11-13 18:49:57 +03:00
2c07b3bed2 Added function for password generation is utils.py 2022-11-13 18:49:14 +03:00
b8113dc47b Sorted imports in utils.py 2022-11-13 18:16:03 +03:00
21bd01c3ed Changed and added comments in the database files 2022-11-10 18:45:10 +03:00
c2280e8bc2 Changed functions for encryption and decryption of accounts to use str of master_pass instead of bytes 2022-11-09 19:02:58 +03:00
f7f954ecd3 Sorted imports 2022-11-09 18:52:59 +03:00
559ee8f6d8 Renamed _memory_use in cryptography into MEMORY_USAGE 2022-11-07 21:02:13 +03:00
13 changed files with 87 additions and 37 deletions

View File

@ -15,8 +15,9 @@ RUN adduser -u 1000 --disabled-password --gecos "" appuser && chown -R appuser /
RUN apt update && apt full-upgrade -y RUN apt update && apt full-upgrade -y
# Install pip requirements # Install pip requirements
RUN pip install -U pip setuptools wheel
COPY requirements.txt . COPY requirements.txt .
RUN python -m pip install -r requirements.txt RUN pip install -r requirements.txt
COPY . /app COPY . /app

View File

@ -23,6 +23,7 @@
- /help - помощь - /help - помощь
- /export - получить пароли в json формате - /export - получить пароли в json формате
- /import - импортировать пароли из json в файле в таком же формате, как из /export - /import - импортировать пароли из json в файле в таком же формате, как из /export
- /gen_password - создать 10 надёжных паролей
### Настройка ### Настройка

View File

@ -1,12 +1,10 @@
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
from sqlalchemy.future import Engine
from . import bot, cryptography, database from . import bot, cryptography, database
__all__ = ["bot", "cryptography", "database"] __all__ = ["bot", "cryptography", "database"]
engine: Engine
def main() -> None: def main() -> None:
@ -16,7 +14,7 @@ def main() -> None:
user=os.getenv("DB_USER"), user=os.getenv("DB_USER"),
passwd=os.getenv("DB_PASS"), passwd=os.getenv("DB_PASS"),
db=os.getenv("DB_NAME"), db=os.getenv("DB_NAME"),
) # type: ignore )
database.prepare.prepare(engine) database.prepare.prepare(engine)
bot_ = bot.create_bot(os.getenv("TG_TOKEN"), engine) # type: ignore bot_ = bot.create_bot(os.getenv("TG_TOKEN"), engine)
bot_.infinity_polling() bot_.infinity_polling()

View File

@ -46,4 +46,7 @@ def create_bot(token: str, engine: Engine) -> telebot.TeleBot:
bot.register_message_handler( bot.register_message_handler(
functools.partial(handlers.import_accounts, bot, engine), commands=["import"] functools.partial(handlers.import_accounts, bot, engine), commands=["import"]
) )
bot.register_message_handler(
functools.partial(handlers.gen_password, bot), commands=["gen_password"]
)
return bot return bot

View File

@ -9,10 +9,11 @@ from .. import cryptography, database
from .utils import ( from .utils import (
accounts_to_json, accounts_to_json,
base_handler, base_handler,
check_account,
check_account_name, check_account_name,
check_login, check_login,
check_passwd, check_passwd,
check_account, gen_passwd,
get_all_accounts, get_all_accounts,
json_to_accounts, json_to_accounts,
send_tmp_message, send_tmp_message,
@ -21,9 +22,7 @@ from .utils import (
Message = telebot.types.Message Message = telebot.types.Message
def get_accounts( def get_accounts(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None:
bot: telebot.TeleBot, engine: Engine, mes: telebot.types.Message
) -> None:
base_handler(bot, mes) base_handler(bot, mes)
accounts = database.get.get_accounts(engine, mes.from_user.id) accounts = database.get.get_accounts(engine, mes.from_user.id)
if not accounts: if not accounts:
@ -41,9 +40,7 @@ def get_accounts(
) )
def delete_all( def delete_all(bot: telebot.TeleBot, engine: Engine, mes: Message) -> None:
bot: telebot.TeleBot, engine: Engine, mes: telebot.types.Message
) -> None:
base_handler(bot, mes) base_handler(bot, mes)
bot_mes = bot.send_message( bot_mes = bot.send_message(
mes.chat.id, mes.chat.id,
@ -225,7 +222,7 @@ def _add_account5(
name, login, passwd = data["name"], data["login"], data["passwd"] name, login, passwd = data["name"], data["login"], data["passwd"]
enc_login, enc_pass, salt = cryptography.other_accounts.encrypt_account_info( enc_login, enc_pass, salt = cryptography.other_accounts.encrypt_account_info(
login, passwd, text.encode("utf-8") login, passwd, text
) )
result = database.add.add_account( result = database.add.add_account(
@ -288,7 +285,7 @@ def _get_account3(
engine, mes.from_user.id, name engine, mes.from_user.id, name
) )
login, passwd = cryptography.other_accounts.decrypt_account_info( login, passwd = cryptography.other_accounts.decrypt_account_info(
enc_login, enc_pass, text.encode("utf-8"), salt enc_login, enc_pass, text, salt
) )
send_tmp_message( send_tmp_message(
bot, bot,
@ -332,7 +329,7 @@ def _delete_account2(
send_tmp_message(bot, mes.chat.id, "Аккаунт удалён") send_tmp_message(bot, mes.chat.id, "Аккаунт удалён")
def help(bot: telebot.TeleBot, mes: telebot.types.Message) -> None: def help(bot: telebot.TeleBot, mes: Message) -> None:
message = """Команды: message = """Команды:
/set_master_pass - установить мастер пароль /set_master_pass - установить мастер пароль
/add_account - создать аккаунт /add_account - создать аккаунт
@ -344,7 +341,8 @@ def help(bot: telebot.TeleBot, mes: telebot.types.Message) -> None:
/cancel - отмена текущего действия /cancel - отмена текущего действия
/help - помощь /help - помощь
/export - получить пароли в json формате /export - получить пароли в json формате
/import - импортировать пароли из json в файле в таком же формате, как из /export""" /import - импортировать пароли из json в файле в таком же формате, как из /export
/gen_password - создать 10 надёжных паролей"""
bot.send_message(mes.chat.id, message) bot.send_message(mes.chat.id, message)
@ -456,7 +454,7 @@ def _import3(
failed.append(name) failed.append(name)
continue continue
enc_login, enc_passwd, salt = cryptography.other_accounts.encrypt_account_info( enc_login, enc_passwd, salt = cryptography.other_accounts.encrypt_account_info(
login, passwd, text.encode("utf-8") login, passwd, text
) )
result = database.add.add_account( result = database.add.add_account(
engine, mes.from_user.id, name, salt, enc_login, enc_passwd engine, mes.from_user.id, name, salt, enc_login, enc_passwd
@ -471,3 +469,15 @@ def _import3(
send_tmp_message(bot, mes.chat.id, mes_text, 10) send_tmp_message(bot, mes.chat.id, mes_text, 10)
del text, mes, accounts del text, mes, accounts
gc.collect() gc.collect()
def gen_password(bot: telebot.TeleBot, mes: Message) -> None:
# Generate 10 passwords and put 'em in the backticks
base_handler(bot, mes)
passwords = (f"`{gen_passwd()}`" for _ in range(10))
text = (
"Пароли:\n"
+ "\n".join(passwords)
+ "\nНажмите на пароль, чтобы его скопировать"
)
send_tmp_message(bot, mes.chat.id, text, 15)

View File

@ -1,15 +1,26 @@
import io import io
import string
import time import time
from random import SystemRandom
from typing import Self, Type from typing import Self, Type
import pydantic import pydantic
import telebot import telebot
from sqlalchemy.future import Engine from sqlalchemy.future import Engine
from .. import database, cryptography from .. import cryptography, database
class Account(pydantic.BaseModel): 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 name: str
login: str login: str
passwd: str passwd: str
@ -23,11 +34,11 @@ class Account(pydantic.BaseModel):
class _Accounts(pydantic.BaseModel): class _Accounts(pydantic.BaseModel):
accounts: list[Account] = pydantic.Field(default_factory=list) accounts: list[_Account] = pydantic.Field(default_factory=list)
def _accounts_list_to_json(accounts: list[tuple[str, str, str]]) -> str: def _accounts_list_to_json(accounts: list[tuple[str, str, str]]) -> str:
accounts = _Accounts(accounts=[Account.from_tuple(i) for i in accounts]) accounts = _Accounts(accounts=[_Account.from_tuple(i) for i in accounts])
return accounts.json() return accounts.json()
@ -36,9 +47,6 @@ def json_to_accounts(json_: str) -> list[tuple[str, str, str]]:
return [i.as_tuple() for i in accounts.accounts] return [i.as_tuple() for i in accounts.accounts]
Message = telebot.types.Message
def send_tmp_message( def send_tmp_message(
bot: telebot.TeleBot, chat_id: telebot.types.Message, text: str, timeout: int = 5 bot: telebot.TeleBot, chat_id: telebot.types.Message, text: str, timeout: int = 5
) -> None: ) -> None:
@ -59,7 +67,6 @@ def get_all_accounts(
engine: Engine, user_id: int, master_pass: str engine: Engine, user_id: int, master_pass: str
) -> list[tuple[str, str, str]]: ) -> list[tuple[str, str, str]]:
accounts: list[tuple[str, str, str]] = [] accounts: list[tuple[str, str, str]] = []
master_pass = master_pass.encode("utf-8")
for account_name in database.get.get_accounts(engine, user_id): for account_name in database.get.get_accounts(engine, user_id):
salt, enc_login, enc_passwd = database.get.get_account_info( salt, enc_login, enc_passwd = database.get.get_account_info(
engine, user_id, account_name engine, user_id, account_name
@ -77,9 +84,9 @@ def accounts_to_json(accounts: list[tuple[str, str, str]]) -> io.StringIO:
return file return file
def _base_check(val: str) -> bool: def _base_check(val: str, /) -> bool:
"Returns false if finds new lines or backtick (`)" "Returns false if finds new lines or backtick (`)"
return not ("\n" in val or "`" in val) return not any(i in FORBIDDEN_CHARS for i in val)
def check_account_name(name: str) -> bool: def check_account_name(name: str) -> bool:
@ -100,3 +107,25 @@ def check_passwd(passwd: str) -> bool:
def check_account(name: str, login: str, passwd: str) -> bool: def check_account(name: str, login: str, passwd: str) -> bool:
"""Runs checks for account name, login and password""" """Runs checks for account name, login and password"""
return check_account_name(name) and check_login(login) and check_passwd(passwd) 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

View File

@ -3,14 +3,14 @@ import os
from cryptography.exceptions import InvalidKey from cryptography.exceptions import InvalidKey
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
_memory_use = 2**14 MEMORY_USAGE = 2**14
def _get_kdf(salt: bytes) -> Scrypt: def _get_kdf(salt: bytes) -> Scrypt:
kdf = Scrypt( kdf = Scrypt(
salt=salt, salt=salt,
length=128, length=128,
n=_memory_use, n=MEMORY_USAGE,
r=8, r=8,
p=1, p=1,
) )

View File

@ -20,12 +20,12 @@ def _generate_key(salt: bytes, master_pass: bytes) -> bytes:
def encrypt_account_info( def encrypt_account_info(
login: str, passwd: str, master_pass: bytes login: str, passwd: str, master_pass: str
) -> tuple[bytes, bytes, bytes]: ) -> tuple[bytes, bytes, bytes]:
"""Encrypts login and password of a user using their master password as a key. """Encrypts login and password of a user using their master password as a key.
Returns a tuple of encrypted login, password and salt""" Returns a tuple of encrypted login, password and salt"""
salt = os.urandom(64) salt = os.urandom(64)
key = _generate_key(salt, master_pass) key = _generate_key(salt, master_pass.encode("utf-8"))
f = Fernet(key) f = Fernet(key)
enc_login = f.encrypt(login.encode("utf-8")) enc_login = f.encrypt(login.encode("utf-8"))
enc_passwd = f.encrypt(passwd.encode("utf-8")) enc_passwd = f.encrypt(passwd.encode("utf-8"))
@ -33,11 +33,11 @@ def encrypt_account_info(
def decrypt_account_info( def decrypt_account_info(
enc_login: bytes, enc_pass: bytes, master_pass: bytes, salt: bytes enc_login: bytes, enc_pass: bytes, master_pass: str, salt: bytes
) -> tuple[str, str]: ) -> tuple[str, str]:
"""Decrypts login and password using their master password as a key. """Decrypts login and password using their master password as a key.
Returns a tuple of decrypted login and password""" Returns a tuple of decrypted login and password"""
key = _generate_key(salt, master_pass) key = _generate_key(salt, master_pass.encode("utf-8"))
f = Fernet(key) f = Fernet(key)
login_bytes = f.decrypt(enc_login) login_bytes = f.decrypt(enc_login)
pass_bytes = f.decrypt(enc_pass) pass_bytes = f.decrypt(enc_pass)

View File

@ -13,7 +13,7 @@ def add_account(
enc_login: bytes, enc_login: bytes,
enc_pass: bytes, enc_pass: bytes,
) -> bool: ) -> bool:
"""Adds account to db. Returns true, if on success""" """Adds account to the database. Returns true on success, false otherwise"""
account = models.Account( account = models.Account(
user_id=user_id, name=name, salt=salt, enc_login=enc_login, enc_pass=enc_pass user_id=user_id, name=name, salt=salt, enc_login=enc_login, enc_pass=enc_pass
) )
@ -28,7 +28,7 @@ def add_account(
def add_master_pass(engine: Engine, user_id: int, salt: bytes, passwd: bytes) -> bool: def add_master_pass(engine: Engine, user_id: int, salt: bytes, passwd: bytes) -> bool:
"""Adds master password to db. Returns true, if on success""" """Adds master password the database. Returns true on success, false otherwise"""
master_pass = models.MasterPass(user_id=user_id, salt=salt, passwd=passwd) master_pass = models.MasterPass(user_id=user_id, salt=salt, passwd=passwd)
try: try:
with sqlmodel.Session(engine) as session: with sqlmodel.Session(engine) as session:

View File

@ -7,6 +7,7 @@ from . import models
def change_master_pass( def change_master_pass(
engine: Engine, user_id: int, salt: bytes, passwd: bytes engine: Engine, user_id: int, salt: bytes, passwd: bytes
) -> None: ) -> None:
"""Changes master password and salt in the database"""
statement = ( statement = (
sqlmodel.update(models.MasterPass) sqlmodel.update(models.MasterPass)
.where(models.MasterPass.user_id == user_id) .where(models.MasterPass.user_id == user_id)

View File

@ -5,6 +5,7 @@ from . import models
def purge_accounts(engine: Engine, user_id: int) -> None: def purge_accounts(engine: Engine, user_id: int) -> None:
"""Deletes all user's accounts"""
statement = sqlmodel.delete(models.Account).where(models.Account.user_id == user_id) statement = sqlmodel.delete(models.Account).where(models.Account.user_id == user_id)
with sqlmodel.Session(engine) as session: with sqlmodel.Session(engine) as session:
session.exec(statement) session.exec(statement)
@ -12,6 +13,7 @@ def purge_accounts(engine: Engine, user_id: int) -> None:
def delete_master_pass(engine: Engine, user_id: int) -> None: def delete_master_pass(engine: Engine, user_id: int) -> None:
"""Delets master password of the user"""
statement = sqlmodel.delete(models.MasterPass).where( statement = sqlmodel.delete(models.MasterPass).where(
models.MasterPass.user_id == user_id models.MasterPass.user_id == user_id
) )
@ -21,6 +23,7 @@ def delete_master_pass(engine: Engine, user_id: int) -> None:
def delete_account(engine: Engine, user_id: int, name: str) -> None: def delete_account(engine: Engine, user_id: int, name: str) -> None:
"""Deletes specific user account"""
statement = sqlmodel.delete(models.Account).where( statement = sqlmodel.delete(models.Account).where(
models.Account.user_id == user_id, models.Account.name == name models.Account.user_id == user_id, models.Account.name == name
) )

View File

@ -5,7 +5,8 @@ from . import models
def get_master_pass(engine: Engine, user_id: int) -> tuple[bytes, bytes] | None: def get_master_pass(engine: Engine, user_id: int) -> tuple[bytes, bytes] | None:
"""Gets master pass. Returns tuple of salt and password""" """Gets master pass. Returns tuple of salt and password
or None if it wasn't found"""
statement = sqlmodel.select(models.MasterPass).where( statement = sqlmodel.select(models.MasterPass).where(
models.MasterPass.user_id == user_id models.MasterPass.user_id == user_id
) )
@ -27,7 +28,8 @@ def get_accounts(engine: Engine, user_id: int) -> list[str]:
def get_account_info( def get_account_info(
engine: Engine, user_id: int, name: str engine: Engine, user_id: int, name: str
) -> tuple[bytes, bytes, bytes]: ) -> tuple[bytes, bytes, bytes]:
"""Gets account info. Returns tuple of salt, login and password""" """Gets account info. Returns tuple of salt, login and password
or None if it wasn't found"""
statement = sqlmodel.select(models.Account).where( statement = sqlmodel.select(models.Account).where(
models.Account.user_id == user_id, models.Account.name == name models.Account.user_id == user_id, models.Account.name == name
) )

View File

@ -5,9 +5,11 @@ from . import models
def get_engine(host: str, user: str, passwd: str, db: str) -> Engine: def get_engine(host: str, user: str, passwd: str, db: str) -> Engine:
"""Creates an engine for mariadb with pymysql as connector"""
engine = sqlmodel.create_engine(f"mariadb+pymysql://{user}:{passwd}@{host}/{db}") engine = sqlmodel.create_engine(f"mariadb+pymysql://{user}:{passwd}@{host}/{db}")
return engine return engine
def prepare(engine: Engine) -> None: def prepare(engine: Engine) -> None:
"""Creates all tables, indexes and constrains in the database"""
sqlmodel.SQLModel.metadata.create_all(engine) sqlmodel.SQLModel.metadata.create_all(engine)