Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ Changes since 2026.06.04
- Adds a new /duck next command to show admins the next duck spawn time
- Fuzzes duck spawn time to make it less predictable

### Grab
- Message content is now encrypted in the database

### Hangman
- /hangman start is no longer logged in discord

Expand Down
80 changes: 80 additions & 0 deletions core/cryptography.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""A few simple functions to handle encrypting data"""

import base64
import hashlib
import hmac
import os

from cryptography.fernet import Fernet


def _get_raw_key() -> bytes:
"""Gets the raw key from the environment.

Raises:
AttributeError: Raised if no key is present.

Returns:
bytes: The raw encryption key.
"""
env_file_key = os.environ.get("DATA_ENCRYPT_KEY")
if not env_file_key:
raise AttributeError("Missing data encryption key, data cannot be secure")

return env_file_key.encode("utf-8")


def _get_key() -> bytes:
"""Processes the raw key into a format that Fernet accepts.

Returns:
bytes: A Fernet-compatible key.
"""
digest = hashlib.sha256(_get_raw_key()).digest()
return base64.urlsafe_b64encode(digest)


def encrypt(text: str) -> str:
"""Encrypts plaintext.

Args:
text (str): The plaintext to encrypt.

Returns:
str: The encrypted text.
"""
fernet_processor = Fernet(_get_key())
token = fernet_processor.encrypt(text.encode("utf-8"))
return token.decode("utf-8")


def decrypt(token: str) -> str:
"""Decrypts encrypted text.

Args:
token (str): The encrypted text to decrypt.

Returns:
str: The decrypted plaintext.
"""
fernet_processor = Fernet(_get_key())
return fernet_processor.decrypt(token.encode("utf-8")).decode("utf-8")


def hash_text(text: str) -> str:
"""Creates a deterministic hash suitable for equality checks.

This should be stored alongside encrypted data to support
duplicate detection and lookups.

Args:
text (str): The plaintext to hash.

Returns:
str: The hexadecimal HMAC digest.
"""
return hmac.new(
_get_raw_key(),
text.encode("utf-8"),
hashlib.sha256,
).hexdigest()
2 changes: 2 additions & 0 deletions core/databases.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ class Grab(bot.db.Model):
channel (str): The channel the message was grabbed from
guild (str): The guild the message was grabbed from
message (str): The string contents of the message
message_hash (str): A hash of the contents of the message
time (datetime.datetime): The time the message was grabbed
nsfw (bool): Whether the message was grabbed in an NSFW channel
"""
Expand All @@ -187,6 +188,7 @@ class Grab(bot.db.Model):
channel: str = bot.db.Column(bot.db.String)
guild: str = bot.db.Column(bot.db.String)
message: str = bot.db.Column(bot.db.String)
message_hash: str = bot.db.Column(bot.db.String)
time: datetime.datetime = bot.db.Column(
bot.db.DateTime, default=datetime.datetime.utcnow
)
Expand Down
3 changes: 2 additions & 1 deletion default.env
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ POSTGRES_DB_NAME=
POSTGRES_DB_PASSWORD=
POSTGRES_DB_USER=
DEBUG=0
CONFIG_YML=./config.yml
CONFIG_YML=./config.yml
DATA_ENCRYPT_KEY=
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ services:
image: rtechsupport/techsupport-bot:prod
build: .
container_name: discordBot
env_file:
- .env
environment:
- DEBUG=${DEBUG}
- TZ=UTC
restart: unless-stopped
stop_signal: SIGINT
Expand Down
17 changes: 11 additions & 6 deletions modules/fun/grab.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import configuration
import ui
from core import auxiliary, cogs
from core import auxiliary, cogs, cryptography

if TYPE_CHECKING:
import bot
Expand Down Expand Up @@ -109,11 +109,13 @@ async def grab_user(
)
return

grab_hash = cryptography.hash_text(grab_message)

grab = (
await self.bot.models.Grab.query.where(
self.bot.models.Grab.author_id == str(user_to_grab.id),
)
.where(self.bot.models.Grab.message == grab_message)
.where(self.bot.models.Grab.message_hash == grab_hash)
.gino.first()
)

Expand All @@ -127,7 +129,8 @@ async def grab_user(
author_id=str(user_to_grab.id),
channel=str(ctx.channel.id),
guild=str(ctx.guild.id),
message=grab_message,
message=cryptography.encrypt(grab_message),
message_hash=grab_hash,
nsfw=ctx.channel.is_nsfw(),
)
await grab.create()
Expand Down Expand Up @@ -206,7 +209,7 @@ async def all_grabs(
else embed
)
embed.add_field(
name=f'"{grab_.message}"',
name=f'"{cryptography.decrypt(grab_.message)}"',
value=grab_.time.date(),
inline=False,
)
Expand Down Expand Up @@ -278,7 +281,7 @@ async def random_grab(
grab = grabs[random_index]

embed = discord.Embed(
title=f'"{grab.message}"',
title=f'"{cryptography.decrypt(grab.message)}"',
description=f"{user_to_grab.name}, {grab.time.date()}",
)

Expand Down Expand Up @@ -320,13 +323,15 @@ async def delete_grab(
channel=ctx.channel,
)
return

grab_hash = cryptography.hash_text(message)
# Gets the target grab by the message
grab = (
await self.bot.models.Grab.query.where(
self.bot.models.Grab.author_id == str(target_user.id)
)
.where(self.bot.models.Grab.guild == str(ctx.guild.id))
.where(self.bot.models.Grab.message == message)
.where(self.bot.models.Grab.message_hash == grab_hash)
.gino.all()
)

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ requires-python = ">=3.13,<3.14"
dependencies = [
"aiocron==2.1",
"bidict==0.23.1",
"cryptography==49.0.0",
"dateparser==1.4.0",
"discord.py==2.7.1",
"emoji==2.15.0",
Expand Down
Loading
Loading