Шифрование токенов

This commit is contained in:
mihalin 2021-09-26 20:36:05 +03:00
parent 188b58d8e2
commit 2e61640f5a
15 changed files with 122 additions and 12 deletions

3
.gitignore vendored
View File

@ -5,4 +5,5 @@ venv
__pycache__ __pycache__
*.pyc *.pyc
config.json config.json
docker-compose-release.yaml docker-compose-release.yaml
docs/build

View File

@ -11,4 +11,5 @@ fi
sleep 10 sleep 10
aerich upgrade aerich upgrade
python migrate.py
python main.py python main.py

View File

@ -5,6 +5,8 @@ POSTGRES_PASSWORD=SOME_RANDOM_PASSWORD_HERE # example: x2y0n27ihiez93kmzj82
POSTGRES_DB=olgram POSTGRES_DB=olgram
POSTGRES_HOST=postgres POSTGRES_HOST=postgres
TOKEN_ENCRYPTION_KEY=SOME_RANDOM_KEY # example:
WEBHOOK_HOST=YOUR_HOST_HERE # example: 11.143.142.140 or my_domain.com WEBHOOK_HOST=YOUR_HOST_HERE # example: 11.143.142.140 or my_domain.com
WEBHOOK_PORT=8443 # allowed: 80, 443, 8080, 8443 WEBHOOK_PORT=8443 # allowed: 80, 443, 8080, 8443
CUSTOM_CERT=true # use that if you don't set up your own domain and let's encrypt certificate CUSTOM_CERT=true # use that if you don't set up your own domain and let's encrypt certificate

View File

@ -47,7 +47,7 @@ def main():
loop.run_until_complete(initialization()) loop.run_until_complete(initialization())
loop.create_task(dp.start_polling()) loop.create_task(dp.start_polling())
loop.create_task(server_main().start()) # loop.create_task(server_main().start())
loop.run_forever() loop.run_forever()

9
migrate.py Normal file
View File

@ -0,0 +1,9 @@
import asyncio
import logging
from olgram.migrations.custom import migrate
logging.basicConfig(level=logging.INFO)
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(migrate())

View File

@ -12,7 +12,7 @@ async def delete_bot(bot: Bot, call: types.CallbackQuery):
Пользователь решил удалить бота Пользователь решил удалить бота
""" """
try: try:
await unregister_token(bot.token) await unregister_token(bot.decrypted_token())
except Unauthorized: except Unauthorized:
# Вероятно пользователь сбросил токен или удалил бот, это уже не наши проблемы # Вероятно пользователь сбросил токен или удалил бот, это уже не наши проблемы
pass pass

View File

@ -99,7 +99,7 @@ async def bot_added(message: types.Message, state: FSMContext):
return await on_unknown_error() return await on_unknown_error()
user, _ = await User.get_or_create(telegram_id=message.from_user.id) user, _ = await User.get_or_create(telegram_id=message.from_user.id)
bot = Bot(token=token, owner=user, name=test_bot_info.username, super_chat_id=message.from_user.id) bot = Bot(token=Bot.encrypted_token(token), owner=user, name=test_bot_info.username, super_chat_id=message.from_user.id)
try: try:
await bot.save() await bot.save()
except IntegrityError: except IntegrityError:

View File

@ -0,0 +1,35 @@
"""Наши собственные миграции, которые нельзя описать на языке SQL и с которыми не справится TortoiseORM/Aerich"""
from tortoise import transactions, Tortoise
from olgram.settings import TORTOISE_ORM
from olgram.models.models import MetaInfo, Bot
import logging
async def upgrade_1():
"""Шифруем токены"""
meta_info = await MetaInfo.first()
if meta_info.version != 0:
logging.info("skip")
return
async with transactions.in_transaction():
bots = await Bot.all()
for bot in bots:
bot.token = bot.encrypted_token(bot.token)
await bot.save()
meta_info.version = 1
await meta_info.save()
logging.info("done")
# Не забудь добавить миграцию в этот лист!
_migrations = [upgrade_1]
async def migrate():
logging.info("Run custom migrations...")
await Tortoise.init(config=TORTOISE_ORM)
for migration in _migrations:
logging.info(f"Migration {migration.__name__}...")
await migration()

View File

@ -0,0 +1,10 @@
-- upgrade --
ALTER TABLE "bot" ALTER COLUMN "token" TYPE VARCHAR(200) USING "token"::VARCHAR(200);
CREATE TABLE IF NOT EXISTS "_custom_meta_info" (
"id" SERIAL NOT NULL PRIMARY KEY,
"version" INT NOT NULL DEFAULT 0
);;
INSERT INTO _custom_meta_info (id, version) VALUES (0, 0) ON CONFLICT DO NOTHING;
-- downgrade --
ALTER TABLE "bot" ALTER COLUMN "token" TYPE VARCHAR(50) USING "token"::VARCHAR(50);
DROP TABLE IF EXISTS "_custom_meta_info";

View File

@ -2,11 +2,27 @@ from tortoise.models import Model
from tortoise import fields from tortoise import fields
from uuid import uuid4 from uuid import uuid4
from textwrap import dedent from textwrap import dedent
from olgram.settings import DatabaseSettings
class MetaInfo(Model):
id = fields.IntField(pk=True)
version = fields.IntField(default=0)
def __init__(self, **kwargs):
# Кажется это единственный способ сделать single-instance модель в TortoiseORM :(
if "id" in kwargs:
kwargs["id"] = 0
self.id = 0
super(MetaInfo, self).__init__(**kwargs)
class Meta:
table = '_custom_meta_info'
class Bot(Model): class Bot(Model):
id = fields.IntField(pk=True) id = fields.IntField(pk=True)
token = fields.CharField(max_length=50, unique=True) token = fields.CharField(max_length=200, unique=True)
owner = fields.ForeignKeyField("models.User", related_name="bots") owner = fields.ForeignKeyField("models.User", related_name="bots")
name = fields.CharField(max_length=33) name = fields.CharField(max_length=33)
code = fields.UUIDField(default=uuid4, index=True) code = fields.UUIDField(default=uuid4, index=True)
@ -22,6 +38,15 @@ class Bot(Model):
on_delete=fields.relational.CASCADE, on_delete=fields.relational.CASCADE,
null=True) null=True)
def decrypted_token(self):
cryptor = DatabaseSettings.cryptor()
return cryptor.decrypt(self.token)
@classmethod
def encrypted_token(cls, token: str):
cryptor = DatabaseSettings.cryptor()
return cryptor.encrypt(token)
async def super_chat_id(self): async def super_chat_id(self):
group_chat = await self.group_chat group_chat = await self.group_chat
if group_chat: if group_chat:

View File

@ -1,18 +1,22 @@
from dotenv import load_dotenv from dotenv import load_dotenv
from abc import ABC from abc import ABC
import os import os
from olgram.utils.crypto import Cryptor
from functools import lru_cache
load_dotenv() load_dotenv()
# TODO: рефакторинг, использовать какой-нибудь lazy-config вместо своих костылей
class AbstractSettings(ABC): class AbstractSettings(ABC):
@classmethod @classmethod
def _get_env(cls, parameter: str, allow_none: bool = False) -> str: def _get_env(cls, parameter: str, allow_none: bool = False) -> str:
parameter = os.getenv(parameter, None) parameter_v = os.getenv(parameter, None)
if not parameter and not allow_none: if not parameter_v and not allow_none:
raise ValueError(f"{parameter} not defined in ENV") raise ValueError(f"{parameter} not defined in ENV")
return parameter return parameter_v
class OlgramSettings(AbstractSettings): class OlgramSettings(AbstractSettings):
@ -99,6 +103,12 @@ class DatabaseSettings(AbstractSettings):
def host(cls) -> str: def host(cls) -> str:
return cls._get_env("POSTGRES_HOST") return cls._get_env("POSTGRES_HOST")
@classmethod
@lru_cache
def cryptor(cls) -> Cryptor:
password = cls._get_env("TOKEN_ENCRYPTION_KEY")
return Cryptor(password)
TORTOISE_ORM = { TORTOISE_ORM = {
"connections": {"default": f'postgres://{DatabaseSettings.user()}:{DatabaseSettings.password()}' "connections": {"default": f'postgres://{DatabaseSettings.user()}:{DatabaseSettings.password()}'

16
olgram/utils/crypto.py Normal file
View File

@ -0,0 +1,16 @@
import base64
from Crypto.Cipher import AES
class Cryptor:
def __init__(self, password: str):
password = password.rjust(32)[:32]
self._cipher = AES.new(password.encode("utf-8"), AES.MODE_ECB)
def encrypt(self, data: str) -> str:
if data.startswith(" "):
raise ValueError("Data should not start with space!")
return base64.b64encode(self._cipher.encrypt(data.encode("utf-8").rjust(64))).decode("utf-8")
def decrypt(self, data: str) -> str:
return self._cipher.decrypt(base64.b64decode(data.encode("utf-8"))).decode("utf-8").lstrip()

View File

@ -4,4 +4,5 @@ aerich==0.5.4
python-dotenv~=0.17.1 python-dotenv~=0.17.1
aioredis==1.3.1 aioredis==1.3.1
aiocache aiocache
aiohttp aiohttp
pycrypto

View File

@ -114,7 +114,7 @@ class CustomRequestHandler(WebhookRequestHandler):
if not bot: if not bot:
return None return None
db_bot_instance.set(bot) db_bot_instance.set(bot)
dp = Dispatcher(AioBot(bot.token)) dp = Dispatcher(AioBot(bot.decrypted_token()))
dp.register_message_handler(message_handler, content_types=[types.ContentType.TEXT, dp.register_message_handler(message_handler, content_types=[types.ContentType.TEXT,
types.ContentType.CONTACT, types.ContentType.CONTACT,

View File

@ -26,9 +26,9 @@ async def register_token(bot: Bot) -> bool:
:param bot: Бот :param bot: Бот
:return: получилось ли :return: получилось ли
""" """
await unregister_token(bot.token) await unregister_token(bot.decrypted_token())
a_bot = AioBot(bot.token) a_bot = AioBot(bot.decrypted_token())
certificate = None certificate = None
if ServerSettings.use_custom_cert(): if ServerSettings.use_custom_cert():
certificate = open(ServerSettings.public_path(), 'rb') certificate = open(ServerSettings.public_path(), 'rb')