feat(core): done full ciphering

master
Romain J 10 months ago
parent 10a19601a1
commit 69426f220b
No known key found for this signature in database
GPG Key ID: 3227578329C2A3A7

@ -1,5 +1,6 @@
import json
from asgiref.sync import sync_to_async
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
from django.templatetags.static import static
@ -7,6 +8,7 @@ from django.urls import reverse
from django.utils.html import escape
from rich import inspect
from chat.apps.guilds.models import Guild
from ..forms import CreateMessageForm
from . import message_types
from ..models import Channel, Message
@ -52,6 +54,8 @@ class ChatConsumer(AsyncWebsocketConsumer):
)
await self.accept()
await self.give_users()
# =========================================================================
async def disconnect(self, close_code):
@ -136,6 +140,21 @@ class ChatConsumer(AsyncWebsocketConsumer):
async def chat_message(self, event):
await self.send(text_data=event["content"])
# =========================================================================
async def give_users(self):
guild = await self._get_guild(self.guild)
public_keys = {}
for member in await self._get_members(guild):
public_keys[str(member.id)] = member.public_key
await self.send(text_data=json.dumps({
"cmd": message_types.OutgoingMessageTypes.GiveMembersPubKeys,
"data": public_keys
}))
# =========================================================================
# =========================================================================
@ -145,6 +164,18 @@ class ChatConsumer(AsyncWebsocketConsumer):
# =========================================================================
@database_sync_to_async
def _get_guild(self, guild_id: str):
return Guild.objects.filter(id=guild_id).first()
# =========================================================================
@sync_to_async
def _get_members(self, guild: Guild):
return list(guild.members.all())
# =========================================================================
@database_sync_to_async
def _can_see(self, channel: Channel):
return self.user.can_see(channel)
@ -154,27 +185,32 @@ class ChatConsumer(AsyncWebsocketConsumer):
@database_sync_to_async
def _make_response(self, message: Message):
return {
"author": {
"id": str(message.author.id),
"avatar_url": escape(
message.author.settings.avatar.url
if message.author.settings.avatar
else static("images/icons/circle_user.svg")
),
"url": reverse(
"users:detail", kwargs={"username": message.author}
),
"name": escape(str(message.author)),
},
"attachments": [
{
"file": {
"name": escape(f.filename),
"url": f.file.url,
"size": f.file.size,
"cmd": "TEXT_MESSAGE",
"data": {
"author": {
"id": str(message.author.id),
"avatar_url": escape(
message.author.settings.avatar.url
if message.author.settings.avatar
else static("images/icons/circle_user.svg")
),
"url": reverse(
"users:detail", kwargs={"username": message.author}
),
"name": escape(str(message.author)),
},
"attachments": [
{
"file": {
"name": escape(f.filename),
"url": f.file.url,
"size": f.file.size,
}
}
}
for f in message.attachments.all()
],
"content": escape(message.content),
for f in message.attachments.all()
],
"recipient": str(message.recipient.id),
"content": escape(message.content),
"nonce": escape(message.nonce),
}
}

@ -11,6 +11,7 @@ class MessageTypes(enum.EnumMeta):
class OutgoingMessageTypes(str, enum.Enum, metaclass=MessageTypes):
TextMessage = "TEXT_MESSAGE"
MessageIdCreated = "MESSAGE_ID_CREATED"
GiveMembersPubKeys = "GIVE_MEMBERS_PUB_KEYS"
NewUnreadCount = "NEW_UNREAD_COUNT"
ErrorOccurred = "ERROR_OCCURRED"
@ -22,71 +23,3 @@ class IngoingMessageTypes(str, enum.Enum, metaclass=MessageTypes):
DeleteMessage = "DELETE_MESSAGE"
ErrorOccurred = "ERROR_OCCURRED"
class OutgoingEventMessageRead(NamedTuple):
message_id: int
sender: str
receiver: str
type: str = "message_read"
def to_json(self) -> str:
return json.dumps(
{
"cmd": IngoingMessageTypes.ReadMessage,
"message_id": self.message_id,
"sender": self.sender,
"receiver": self.receiver,
}
)
class OutgoingEventNewMessage(NamedTuple):
random_id: int
text: str
sender: str
receiver: str
sender_username: str
type: str = "new_text_message"
def to_json(self) -> str:
return json.dumps(
{
"cmd": OutgoingMessageTypes.TextMessage,
"random_id": self.random_id,
"text": self.text,
"sender": self.sender,
"receiver": self.receiver,
"sender_username": self.sender_username,
}
)
class OutgoingEventNewUnreadCount(NamedTuple):
sender: str
unread_count: int
type: str = "new_unread_count"
def to_json(self) -> str:
return json.dumps(
{
"cmd": OutgoingMessageTypes.NewUnreadCount,
"sender": self.sender,
"unread_count": self.unread_count,
}
)
class OutgoingEventMessageIdCreated(NamedTuple):
random_id: int
db_id: int
type: str = "message_id_created"
def to_json(self) -> str:
return json.dumps(
{
"cmd": OutgoingMessageTypes.MessageIdCreated,
"random_id": self.random_id,
"db_id": self.db_id,
}
)

@ -9,7 +9,7 @@ class CreateMessageForm(forms.ModelForm):
class Meta:
model = Message
fields = ["author", "channel", "content", "attachments"]
fields = ["author", "channel", "recipient", "content", "nonce"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

@ -62,10 +62,7 @@ class Channel(models.Model):
# =========================================================================
def get_messages(self, recipient: User):
return Message.objects.filter(channel=self)
# def messages_count(self):
# return len(self.get_messages().all())
return Message.objects.filter(channel=self, recipient=recipient)
# =========================================================================
@ -88,6 +85,8 @@ class Message(models.Model):
)
content = models.TextField()
nonce = models.TextField()
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="author"
)

@ -5,6 +5,7 @@ from django.core.handlers.asgi import ASGIRequest
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect, render
from .models import Channel
from ...models import Guild
from ...utils import get_guild
from ...views import BaseGuildView, template_path
@ -71,12 +72,13 @@ class GuildChannelDetailView(BaseChannelView):
params = get_params(request, guild_id)
guild: Guild = params["guild"]
channel = guild.channels.get(id__exact=channel_id)
channel: Channel = guild.channels.get(id__exact=channel_id)
if not channel:
return redirect("guild:guild_details", guild_id=str(guild_id))
params["room_name"] = channel
params["room_messages"] = channel.get_messages(request.user)
return render(request, self.template_name, params)

@ -0,0 +1,19 @@
# Generated by Django 3.2.8 on 2022-05-25 16:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guilds', '0002_initial'),
]
operations = [
migrations.AddField(
model_name='message',
name='nonce',
field=models.TextField(default=''),
preserve_default=False,
),
]

@ -32,6 +32,18 @@ function createAttachments(attachments) {
}
function createMessage(data) {
const sharedKey = nacl.box.before(
nacl.util.decodeBase64(recipients[data.author.id]), usableKeyPair.secretKey
);
const message = {
box: nacl.util.decodeBase64(data.content),
nonce: nacl.util.decodeBase64(data.nonce)
}
const payload = nacl.box.open.after(message.box, message.nonce, sharedKey);
return `
<article class="uk-alert uk-comment uk-position-relative uk-padding-small uk-margin-small uk-border-rounded
message ${data.author.id === me ? 'message-me uk-alert-success' : 'uk-alert-primary'}
@ -59,7 +71,7 @@ function createMessage(data) {
</header>
<div class="uk-comment-body">
<p>${decipher(data.content, data.author.id)}</p>
<p>${decipher({box: data.content, nonce: data.nonce}, data.author.id)}</p>
</div>
</article>`;
}

@ -1,9 +1,19 @@
/* Project specific Javascript goes here. */
function cipher(text) {
return CryptoJS.AES.encrypt(text, me).toString();
function constructKeyPair(keyPair) {
return nacl.box.keyPair.fromSecretKey(nacl.util.decodeBase64(keyPair[1]));
}
function decipher(text, key) {
return CryptoJS.AES.decrypt(text, key).toString(CryptoJS.enc.Utf8);
function decipher(message, author) {
const sharedKey = nacl.box.before(
nacl.util.decodeBase64(recipients[author]), usableKeyPair.secretKey
);
const payload = nacl.box.open.after(
nacl.util.decodeBase64(message.box),
nacl.util.decodeBase64(message.nonce),
sharedKey
);
return nacl.util.encodeUTF8(payload);
}

@ -1 +1 @@
function cipher(t){return CryptoJS.AES.encrypt(t,me).toString()}function decipher(t,r){return CryptoJS.AES.decrypt(t,r).toString(CryptoJS.enc.Utf8)}
function constructKeyPair(e){return nacl.box.keyPair.fromSecretKey(nacl.util.decodeBase64(e[1]))}function decipher(e,c){const n=nacl.box.before(nacl.util.decodeBase64(recipients[c]),usableKeyPair.secretKey),a=nacl.box.open.after(nacl.util.decodeBase64(e.box),nacl.util.decodeBase64(e.nonce),n);return nacl.util.encodeUTF8(a)}

@ -0,0 +1,61 @@
document.addEventListener("DOMContentLoaded", () => {
let fail_count = 0;
function process_message(data) {
let cmd = data.cmd;
let payload = data.data;
if (!(cmd && payload)) {
console.error("Improper ws message received");
return;
}
switch (cmd) {
case "GIVE_MEMBERS_PUB_KEYS":
window.recipients = payload;
break;
case "MEMBER_JOIN":
window.recipients = {...payload, ...recipients};
break;
case "MEMBER_LEAVE":
delete window.recipients[payload];
break;
case "TEXT_MESSAGE":
add_message(payload);
break;
}
}
(function get_websocket() {
let chatSocket = new WebSocket(
'ws://'
+ '127.0.0.1:8000'
+ '/ws'
+ `/${room_name.guild.id}`
+ `/${room_name.id}`
);
chatSocket.onmessage = (e) => {
const data = JSON.parse(e.data);
process_message(data)
};
chatSocket.onopen = () => {
console.log('Websocket connected');
fail_count = 0
};
chatSocket.onclose = () => {
if (fail_count > 2) {
UIkit.modal(document.getElementById("ws_error_modal")).show();
} else {
fail_count += 1;
console.error('Chat socket closed unexpectedly');
console.info('Trying to reconnect in 5s...');
setTimeout(() => get_websocket(), 5000)
}
};
window.ws = chatSocket;
})();
});

@ -15,7 +15,7 @@
{% block content %}
<div class="uk-padding-small uk-card-body uk-overflow-auto" id="messages"
uk-height-viewport="offset-top: true; offset-bottom: true">
{% with messages=room_name.get_messages.all %}
{% with messages=room_messages %}
{% if messages %}
{% for message in messages %}
{% include "layouts/components/message/block.html" with message=message %}
@ -31,9 +31,9 @@
<div class="uk-card-footer">
<form id="message_form">
<label class="uk-flex">
{# <button class="uk-button uk-button-default uk-padding-small uk-padding-remove-vertical">#}
{# <span uk-icon="icon: plus-circle"></span>#}
{# </button>#}
{# <button class="uk-button uk-button-default uk-padding-small uk-padding-remove-vertical">#}
{# <span uk-icon="icon: plus-circle"></span>#}
{# </button>#}
<textarea id="message_input" class="uk-textarea"
style="resize: none; max-height: 20vh" autofocus
@ -51,56 +51,81 @@
{% block inline_javascript %}
<script>
const messages = document.getElementById("messages");
const textarea = document.getElementById("message_input");
const attachments = undefined;
document.addEventListener("DOMContentLoaded", () => {
if (window.keyPair === undefined) {
window.keyPair = window.sessionStorage.getItem("keySet").split("|");
}
const form = document.getElementById("message_form");
window.usableKeyPair = constructKeyPair(keyPair);
const messages = document.getElementById("messages");
const textarea = document.getElementById("message_input");
const attachments = undefined;
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutationRecord) {
const style = mutationRecord.target.style;
const form = document.getElementById("message_form");
if (style) {
style.maxHeight = style.minHeight;
}
messages.scrollTop = messages.scrollHeight;
});
});
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutationRecord) {
const style = mutationRecord.target.style;
observer.observe(messages, {attributes: true, attributeFilter: ['style']});
if (style) {
style.maxHeight = style.minHeight;
}
messages.scrollTop = messages.scrollHeight;
});
});
function send_message() {
window.ws.send(JSON.stringify({
"cmd": "POST_MESSAGE",
"data": {
"content": cipher(textarea.value),
"attachments": attachments
observer.observe(messages, {attributes: true, attributeFilter: ['style']});
function send_message() {
for (const [recipient_id, recipient_pubKey] of Object.entries(recipients)) {
const sharedKey = nacl.box.before(nacl.util.decodeBase64(recipient_pubKey), usableKeyPair.secretKey);
const nonce = nacl.randomBytes(24)
const box = nacl.box.after(
nacl.util.decodeUTF8(textarea.value),
nonce,
sharedKey
)
const message = {box, nonce}
ws.send(JSON.stringify({
cmd: "POST_MESSAGE",
data: {
content: nacl.util.encodeBase64(message.box),
nonce: nacl.util.encodeBase64(message.nonce),
recipient: recipient_id
}
}))
}
}));
textarea.value = "";
}
function add_message(data) {
const new_message = createMessage(data);
messages.insertAdjacentHTML("beforeend", new_message);
messages.scrollTop = messages.scrollHeight;
}
textarea.value = "";
}
form.addEventListener("submit", (e) => {
e.preventDefault();
send_message();
});
function add_message(data) {
if (data.recipient !== me) return;
textarea.addEventListener("keyup", (e) => {
if (e.key === "Enter") {
if (e.ctrlKey) textarea.value += "\n";
else send_message();
const new_message = createMessage(data);
messages.insertAdjacentHTML("beforeend", new_message);
messages.scrollTop = messages.scrollHeight;
}
form.addEventListener("submit", (e) => {
e.preventDefault();
send_message();
});
textarea.addEventListener("keyup", (e) => {
if (e.key === "Enter") {
if (e.ctrlKey) textarea.value += "\n";
else send_message();
}
});
window.add_message = add_message
});
</script>

@ -10,8 +10,6 @@
<thead>
<tr>
<th scope="col" class="uk-width-auto">{% trans "Channel" %}</th>
<th scope="col" class="uk-width-expand">{% trans "Last message" %}</th>
<th scope="col">{% trans "Messages" %}</th>
</tr>
</thead>
<tbody>
@ -22,21 +20,6 @@
<a class="uk-link"
href="{% url 'guild:channel_details' guild.id channel.id %}">{{ channel }}</a>
</td>
<td class="uk-overflow-auto">
{# {% if channel.last_message %}#}
{# {% include "layouts/components/message/block.html" with message=channel.messages.first %}#}
{# <b class="uk-text-bold">[{{ channel.last_message.author }}]</b>#}
{# <span id="prev_{{ channel.last_message.id }}">{{ channel.last_message.content }}</span>#}
{# <script>#}
{# window.addEventListener('load', () => {#}
{# document.getElementById("prev_{{ channel.last_message.id }}").innerText = decipher('{{ channel.last_message.content }}', '{{ channel.last_message.author.id }}');#}
{# });#}
{# </script>#}
{# {% endif %}#}
</td>
<td>
{# <span>{{ channel.messages_count }}</span>#}
</td>
</tr>
{% endfor %}
{% else %}

@ -103,51 +103,19 @@
</div>
<script>
let fail_count = 0;
(function get_websocket() {
{#let chatSocket = new WebSocket(#}
{# 'wss://'#}
{# + 'c3e.gnous.eu'#}
{# + '/ws'#}
{# + '/{{ room_name.guild.id }}'#}
{# + '/{{ room_name.id }}'#}
{#); #}
let chatSocket = new WebSocket(
'ws://'
+ '127.0.0.1:8000'
+ '/ws'
+ '/{{ room_name.guild.id }}'
+ '/{{ room_name.id }}'
);
chatSocket.onmessage = (e) => {
const data = JSON.parse(e.data);
add_message(data)
};
chatSocket.onopen = () => {
console.log('Websocket connected');
fail_count = 0
};
chatSocket.onclose = () => {
if (fail_count > 2) {
UIkit.modal(document.getElementById("ws_error_modal")).show();
} else {
fail_count += 1;
console.error('Chat socket closed unexpectedly');
console.info('Trying to reconnect in 5s...');
setTimeout(() => get_websocket(), 5000)
}
};
window.ws = chatSocket;
})();
window.room_name = {
id: '{{ room_name.id }}',
guild: {
name: '{{ room_name.guild.name }}',
id: '{{ room_name.guild.id }}',
},
}
</script>
{% compress js %}
<script src="{% static 'js/ws.js' %}"></script>
{% endcompress %}
<script>
window.me = '{{ user.id }}'
window.me = '{{ user.id }}'
</script>
{% endif %}

@ -49,10 +49,15 @@
</header>
<div class="uk-comment-body">
<p id="{{ message.id }}">{{ message.content }}</p>
<p id="{{ message.id }}">{% trans "Loading..." %}</p>
<script>
window.addEventListener('load', () => {
document.getElementById("{{ message.id }}").innerText = decipher('{{ message.content }}', '{{ message.author.id }}');
document.getElementById("{{ message.id }}").innerText = (
decipher(
{box: '{{ message.content }}', nonce: '{{ message.nonce }}'},
'{{ message.author.id }}'
)
);
});
</script>
</div>

@ -62,7 +62,7 @@
const keyPair = window.sessionStorage.getItem("keySet").split("|");
document.querySelectorAll("input[name='public_key']").forEach(input => {
input.value = keyPair[1];
input.value = keyPair[0];
});
</script>
{% endblock inline_javascript %}

@ -328,3 +328,5 @@ CORS_ALLOWED_ORIGINS = ["https://c3e.gnous.eu", "http://127.0.0.1:8000", "http:/
CHANNEL_LAYERS = {
"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}
}
DJANGO_ALLOW_ASYNC_UNSAFE = True

Loading…
Cancel
Save