Host our public key, and add proper ways to sign JSON + send requests.
This commit is contained in:
@@ -19,7 +19,7 @@ room_dir_room = {
|
||||
"world_readable": False
|
||||
}
|
||||
],
|
||||
"total_room_count_estimate": 43502
|
||||
"total_room_count_estimate": 1
|
||||
}
|
||||
|
||||
# Where users should reach out for support.
|
||||
@@ -49,9 +49,7 @@ users_can_register = False
|
||||
# The funny number.
|
||||
the_funny_number = 1337
|
||||
|
||||
# Your private key for Vona.
|
||||
# Your private key for Vona, in the format of:
|
||||
# ed25519 <key id> <base64 encoded key>
|
||||
#
|
||||
# To generate a private key:
|
||||
# openssl genpkey -algorithm Ed25519 -out privkey.pem
|
||||
#
|
||||
ed25519_key_path = "/etc/vonakey.pem"
|
||||
signing_key = ""
|
||||
|
||||
105
src/globals.py
105
src/globals.py
@@ -1,50 +1,107 @@
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import nacl.signing
|
||||
import hashlib
|
||||
import base64
|
||||
import config
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
|
||||
vona_version = "1.2.4"
|
||||
|
||||
|
||||
def canonical_json(value):
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
separators=(',',':'),
|
||||
sort_keys=True
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
).encode("UTF-8")
|
||||
|
||||
'''
|
||||
def encode_base64(data: bytes) -> str:
|
||||
return base64.b64encode(data).decode('utf-8')
|
||||
|
||||
def sign_json(json_object, signing_key, signing_name):
|
||||
signatures = json_object.pop("signatures", {})
|
||||
unsigned = json_object.pop("unsigned", None)
|
||||
def sign_json(data):
|
||||
parts = config.signing_key.split()
|
||||
base64_key = parts[2]
|
||||
|
||||
signed = signing_key.sign(canonical_json(json_object))
|
||||
signature_base64 = encode_base64(signed)
|
||||
while len(base64_key) % 4 != 0:
|
||||
base64_key += "="
|
||||
|
||||
key_id = "ed25519:VonaA"
|
||||
signatures.setdefault(signing_name, {})[key_id] = signature_base64
|
||||
decoded_key = base64.b64decode(base64_key)
|
||||
signing_key = nacl.signing.SigningKey(decoded_key)
|
||||
|
||||
json_object["signatures"] = signatures
|
||||
if unsigned is not None:
|
||||
json_object["unsigned"] = unsigned
|
||||
signed_message = signing_key.sign(canonical_json(data))
|
||||
|
||||
return json_object
|
||||
'''
|
||||
signature = signed_message.signature
|
||||
|
||||
key_version = parts[1]
|
||||
signature_base64 = base64.b64encode(signature).decode("utf-8").rstrip("=")
|
||||
|
||||
signed_json = {
|
||||
**data,
|
||||
"signatures": {
|
||||
config.server_name: {
|
||||
f"ed25519:{key_version}": signature_base64,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return signed_json
|
||||
|
||||
vona_version = '1.2.3'
|
||||
|
||||
def make_event_id():
|
||||
return re.sub(r'[\/+=]', '_', base64.b64encode(os.urandom(32)).decode('utf-8'))[:44]
|
||||
return re.sub(r"[\/+=]", "_", base64.b64encode(os.urandom(32)).decode("utf-8"))[:44]
|
||||
|
||||
|
||||
def hash_event(input) -> str:
|
||||
input.pop('signatures', None)
|
||||
input.pop('unsigned', None)
|
||||
input.pop("signatures", None)
|
||||
input.pop("unsigned", None)
|
||||
|
||||
sha256_hash = hashlib.sha256(canonical_json(input)).digest()
|
||||
base64_encoded = base64.b64encode(sha256_hash)
|
||||
|
||||
return base64_encoded.decode().rstrip('=')
|
||||
return base64_encoded.decode().rstrip("=")
|
||||
|
||||
|
||||
def pubkey() -> str:
|
||||
private_key = config.signing_key.split()[2]
|
||||
|
||||
while len(private_key) % 4 != 0:
|
||||
private_key += "="
|
||||
|
||||
public_key = nacl.signing.SigningKey(base64.b64decode(private_key)).verify_key
|
||||
|
||||
return (
|
||||
public_key.encode(encoder=nacl.encoding.Base64Encoder)
|
||||
.decode("utf-8")
|
||||
.rstrip("=")
|
||||
)
|
||||
|
||||
|
||||
def make_auth_header(destination, method, path, content=None) -> str:
|
||||
request_json = {
|
||||
"method": method,
|
||||
"uri": path,
|
||||
"origin": config.server_name,
|
||||
"destination": destination,
|
||||
}
|
||||
|
||||
if content is not None:
|
||||
request_json["content"] = content
|
||||
|
||||
signed_json = sign_json(request_json)
|
||||
|
||||
authorization_headers = []
|
||||
|
||||
for key, sig in signed_json["signatures"][origin_name].items():
|
||||
authorization_headers.append(
|
||||
bytes(
|
||||
'X-Matrix origin="%s",destination="%s",key="%s",sig="%s"'
|
||||
% (
|
||||
config.server_name,
|
||||
destination,
|
||||
key,
|
||||
sig,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return ("Authorization", authorization_headers[0])
|
||||
|
||||
@@ -26,7 +26,7 @@ async def root():
|
||||
|
||||
@app.route("/_matrix/static/")
|
||||
async def matrix_static():
|
||||
return f'<!DOCTYPE html><html lang="en"><head><title>Vona {globals.vona_version} is running</title><style>body {{font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;max-width: 40em;margin: auto;text-align: center;}}h1,p {{margin: 1.5em;}}hr {{border: none;background-color: #ccc;color: #ccc;height: 1px;width: 7em;margin-top: 4em;}}.logo {{display: block;width: 12em;height: auto;margin: 4em auto;}}</style></head><body><img src="https://matrix.org/images/matrix-logo.svg" class="logo"><h1>It works! Vona {globals.vona_version} is running</h1><p>Your Vona server is listening on this port and is ready for messages.</p><p>To use this server you\'ll need <a href="https://matrix.org/ecosystem/clients/" target="_blank"rel="noopener noreferrer">a Matrix client</a>.</p><p>Welcome to the Matrix universe :)</p><hr><p><small><a href="https://matrix.org" target="_blank" rel="noopener noreferrer">matrix.org</a></small></p></body></html>'
|
||||
return f'<!DOCTYPE html><html lang="en"><head><title>Vona {globals.vona_version} is running</title><style>body {{font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;max-width: 40em;margin: auto;text-align: center;}}h1,p {{margin: 1.5em;}}hr {{border: none;background-color: #ccc;color: #ccc;height: 1px;width: 7em;margin-top: 4em;}}.logo {{display: block;width: 12em;height: auto;margin: 4em auto;}}</style></head><body><img src="https://matrix.org/images/matrix-logo.svg" class="logo"><h1>It works! Vona {globals.vona_version} is running</h1><p>Your Vona server is listening on this port and is ready for messages.</p><p>To use this server you\'ll need <a href="https://matrix.org/ecosystem/clients/" target="_blank"rel="noopener noreferrer">a Matrix client</a>.</p><p>Welcome to the Matrix universe :)</p><hr><p><small><a href="https://natribu.org/en/" target="_blank" rel="noopener noreferrer">matrix.org</a></small></p></body></html>'
|
||||
|
||||
|
||||
# Error handlers
|
||||
@@ -44,7 +44,7 @@ async def internal_error(error):
|
||||
|
||||
|
||||
# Well-known endpoints for federation,
|
||||
# clients, and support information
|
||||
# clients, and support information.
|
||||
@app.route("/.well-known/matrix/server")
|
||||
async def server():
|
||||
return jsonify({"m.server": f"{config.server_name}:443"})
|
||||
|
||||
68
src/s2s.py
68
src/s2s.py
@@ -1,21 +1,20 @@
|
||||
from flask import Flask, jsonify, Response, request, send_file, abort, Blueprint
|
||||
from globals import vona_version, make_event_id, hash_event
|
||||
from config import *
|
||||
import requests
|
||||
import globals
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
|
||||
server = Blueprint('matrix_server', __name__)
|
||||
|
||||
def send_join(request, roomId):
|
||||
# We may have to include signatures here
|
||||
# as well, which will be a pain
|
||||
# We may have to include signatures here.
|
||||
|
||||
eventIds = [
|
||||
f"${make_event_id()}:{server_name}",
|
||||
f"${make_event_id()}:{server_name}",
|
||||
f"${make_event_id()}:{server_name}",
|
||||
f"${make_event_id()}:{server_name}"
|
||||
f"${globals.make_event_id()}:{server_name}",
|
||||
f"${globals.make_event_id()}:{server_name}",
|
||||
f"${globals.make_event_id()}:{server_name}",
|
||||
f"${globals.make_event_id()}:{server_name}"
|
||||
]
|
||||
|
||||
events = {
|
||||
@@ -67,7 +66,7 @@ def send_join(request, roomId):
|
||||
}
|
||||
|
||||
for event in events['state']:
|
||||
event_hash = hash_event(event)
|
||||
event_hash = globals.hash_event(event)
|
||||
if event['type'] != "m.room.create":
|
||||
event['prev_events'] = [eventIds[event['origin_server_ts'] - 1], {"sha256": event_hash}]
|
||||
|
||||
@@ -75,6 +74,7 @@ def send_join(request, roomId):
|
||||
join_event['prev_events'] = [eventIds[3], {"sha256": events['state'][3]['prev_events'][1]['sha256']}]
|
||||
events['event'] = join_event
|
||||
|
||||
# debug
|
||||
print(json.dumps(events, indent='\t'))
|
||||
print(json.dumps(eventIds, indent='\t'))
|
||||
|
||||
@@ -82,12 +82,20 @@ def send_join(request, roomId):
|
||||
|
||||
@server.route('/_matrix/federation/v1/version')
|
||||
def version():
|
||||
return jsonify({"server": {"version": vona_version,"name": "Vona"}})
|
||||
return jsonify({"server": {"version": globals.vona_version,"name": "Vona"}})
|
||||
|
||||
@server.route('/_matrix/key/v2/server')
|
||||
def keys():
|
||||
# todo
|
||||
return jsonify({})
|
||||
return jsonify(globals.sign_json({
|
||||
"old_verify_keys": {},
|
||||
"server_name": server_name,
|
||||
"valid_until_ts": int(time.time() * 1000 + 604800000),
|
||||
"verify_keys": {
|
||||
f"ed25519:{signing_key.split()[1]}": {
|
||||
"key": globals.pubkey()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
@server.route('/_matrix/federation/v1/query/directory')
|
||||
def room_query():
|
||||
@@ -116,6 +124,7 @@ def download_media(media_id):
|
||||
|
||||
response = Response(response_body, content_type=f'multipart/mixed; boundary={boundary}')
|
||||
response.status_code = 200
|
||||
|
||||
return response
|
||||
|
||||
@server.route('/_matrix/federation/v1/media/thumbnail/<media_id>')
|
||||
@@ -182,6 +191,7 @@ def user_profile():
|
||||
return jsonify({"displayname":"Vona"})
|
||||
else:
|
||||
return jsonify({"errcode": "M_NOT_FOUND","error": "The requested profile key does not exist."}), 404
|
||||
|
||||
return jsonify({"avatar_url": f"mxc://{server_name}/cat","displayname": "Vona"})
|
||||
|
||||
@server.route('/_matrix/federation/v1/user/devices/<user>')
|
||||
@@ -196,9 +206,39 @@ def user_devices(user):
|
||||
def user_keys():
|
||||
return jsonify({"device_keys":{f"@vona:{server_name}":{}}})
|
||||
|
||||
|
||||
# https://spec.matrix.org/v1.15/server-server-api/#inviting-to-a-room
|
||||
@server.route('/_matrix/federation/v2/invite/<room>/<txnId>', methods=['PUT'])
|
||||
def fed_invite_user(room, txnId):
|
||||
return jsonify({"errcode":"M_INCOMPATIBLE_ROOM_VERSION","error": "Don't touch my users mofo","room_version": str(the_funny_number)}), 400
|
||||
def invite_user_v2(room, txnId):
|
||||
# This endpoint is not allowed for room
|
||||
# versions over v2 as per spec, so any
|
||||
# invites received here can be discarded
|
||||
# safely.
|
||||
|
||||
return jsonify({
|
||||
"errcode": "M_INCOMPATIBLE_ROOM_VERSION",
|
||||
"error": "Vona only supports room version 2.",
|
||||
"room_version": str(the_funny_number)
|
||||
}), 400
|
||||
|
||||
@server.route('/_matrix/federation/v1/invite/<room>/<txnId>', methods=['PUT'])
|
||||
def invite_user(room, txnId):
|
||||
# TODO: Sign provided JSON and return it.
|
||||
|
||||
data = request.data.decode('utf-8')
|
||||
invite_data = json.loads(data)
|
||||
|
||||
if "room_version" in invite_data:
|
||||
if invite_data["room_version"] != "2":
|
||||
return jsonify({
|
||||
"errcode": "M_INCOMPATIBLE_ROOM_VERSION",
|
||||
"error": "Vona only supports room version 2.",
|
||||
"room_version": invite_data["room_version"]
|
||||
})
|
||||
|
||||
# Placeholder
|
||||
abort(500)
|
||||
|
||||
|
||||
@server.route('/_matrix/federation/v1/hierarchy/<roomId>')
|
||||
def space_hierachy(roomId):
|
||||
|
||||
Reference in New Issue
Block a user