From 9bc36f324a37c0a8d5bb8f52c55c294a3a431b3d Mon Sep 17 00:00:00 2001 From: lda Date: Wed, 8 May 2024 12:14:10 +0200 Subject: [PATCH] [ADD/WIP/UNTESTED] Start verifying membership, tested hashing --- src/CanonicalJson.c | 27 ++-- src/Room.c | 369 ++++++++++++++++++++++++++++++++++++++++---- src/State.c | 36 ++++- src/include/Room.h | 8 + src/include/State.h | 5 + 5 files changed, 408 insertions(+), 37 deletions(-) diff --git a/src/CanonicalJson.c b/src/CanonicalJson.c index 000d952..5847ef2 100644 --- a/src/CanonicalJson.c +++ b/src/CanonicalJson.c @@ -168,23 +168,29 @@ CanonicalJsonEncode(HashMap * object, Stream * out) static ssize_t StringIoRead(void *cookie, void *buf, size_t count) { + (void) cookie; + (void) buf; + (void) count; return -1; /* TODO: Consider reading properly */ } static ssize_t StringIoWrite(void *c, void *buf, size_t count) { - char **str = c; - char *stringised = Malloc(count + 1); - char *new; - memcpy(stringised, buf, count); - new = StrConcat(2, *str, stringised); - - Free(stringised); - if (*str) + if (count > 0) { - Free(*str); + char **str = c; + char *stringised = Malloc(count + 1); + char *new; + memcpy(stringised, buf, count); + new = StrConcat(2, *str, stringised); + + Free(stringised); + if (*str) + { + Free(*str); + } + *str = new; } - *str = new; return count; } static const IoFunctions Functions = { @@ -202,6 +208,7 @@ CanonicalJsonHash(HashMap *json) Io *string_writer = IoCreate(&string, Functions); Stream *io_stream = StreamIo(string_writer); CanonicalJsonEncode(json, io_stream); + StreamFlush(io_stream); sha = Sha256(string); shastr = ShaToHex(sha); diff --git a/src/Room.c b/src/Room.c index 6369d4a..7b5ad6d 100644 --- a/src/Room.c +++ b/src/Room.c @@ -23,9 +23,11 @@ * SOFTWARE. */ -#include "Cytoplasm/Json.h" -#include "Cytoplasm/Stream.h" +#include +#include +/*#include "Cytoplasm/Stream.h"*/ #include "Parser.h" +#include "User.h" #include #include @@ -42,7 +44,10 @@ #include #include +#include +#define IsState(p, typ, key) (StrEquals(p->type, typ) && \ + StrEquals(p->state_key, key)) struct Room { Db *db; @@ -84,7 +89,6 @@ RoomCreate(Db * db, User *user, RoomCreateRequest * req) room->id = GenerateRoomId(); /* TODO: Check config. */ room->version = version_num; - /* TODO: A room is *not just* its state? */ room->ref = DbCreate(db, 3, "rooms", room->id, "state"); /* TODO: Populate room with information */ @@ -185,23 +189,12 @@ PopulateEventV1(Room * room, HashMap * event, PduV1 * pdu) pdu->content = JsonDuplicate(event); /* Copy the original JSON data */ /* TODO: Signature and alldat. */ - return false; -} -static bool -PopulateEventV3(Room * room, HashMap * event, PduV3 *pdu) -{ - char *unused; - if (PduV3FromJson(event, pdu, &unused)) - { - return true; - } - /* TODO: Create a PDU of our own, signed and everything. - * https://telodendria.io/blog/matrix-protocol-overview - * has some ideas on how this could be done(up until stage 5). */ + (void) room; return false; } -static AuthoriseCreateV1(PduV1 pdu) +static bool +AuthoriseCreateV1(PduV1 pdu) { bool ret = true; CommonID sender = { 0 }, room_id = { 0 }; @@ -244,23 +237,298 @@ finish: CommonIDFree(room_id); return ret; } -static bool -AuthoriseEventV1(PduV1 pdu) + +static bool +ValidAuthEventV1(PduV1 *auth_pdu, PduV1 *pdu) { + if (IsState(auth_pdu, "m.room.create", "")) + { + return true; + } + if (IsState(auth_pdu, "m.room.power_levels", "")) + { + /* TODO: Check if it's the latest in terms of [pdu] */ + return true; + } + if (StrEquals(pdu->type, "m.room.member")) + { + char *membership = + JsonValueAsString(JsonGet(pdu->content, 1, "membership")); + if (IsState(auth_pdu, "m.room.member", pdu->sender)) + { + /* TODO: Check if it's the latest in terms of [pdu] */ + return true; + } + if ((StrEquals(membership, "join") || + StrEquals(membership, "invite")) && + IsState(auth_pdu, "m.room.join_rules","")) + { + /* TODO: Check if it's the latest in terms of [pdu] */ + return true; + } + /* TODO: The rest. + * https://spec.matrix.org/v1.7/server-server-api/#auth-events-selection */ + } + return false; +} +static bool +VerifyPDUV1(PduV1 *auth_pdu) +{ + /* TODO: + * https://spec.matrix.org/v1.7/server-server-api/ + * #checks-performed-on-receipt-of-a-pdu */ + (void) auth_pdu; + return false; /* This only shows whenever an event was rejected, not + * soft-failed */ +} +static bool +ConsiderAuthEventsV1(Room * room, PduV1 pdu) +{ + char *ignored; size_t i; + HashMap *state_keytype; + state_keytype = HashMapCreate(); + for (i = 0; i < ArraySize(pdu.auth_events); i++) + { + char *event_id = ArrayGet(pdu.auth_events, i); + HashMap *event = RoomEventFetch(room, event_id); + PduV1 auth_pdu; + + char *key_type_id; + + if (!PduV1FromJson(event, &auth_pdu, &ignored)) + { + HashMapFree(event); + return false; /* Yeah... we aren't doing that. */ + } + + /* TODO: Find a better way to do this. Using HashMaps as sets + * *works*, but it's not the best of ideas here. Also, we're using + * strings to compare things, which yeah. */ + key_type_id = StrConcat(3, auth_pdu.type, ",", auth_pdu.state_key); + + if (HashMapGet(state_keytype, key_type_id)) + { + /* Duplicate found! We actually ignore it's actual value. */ + HashMapFree(event); + PduV1Free(&auth_pdu); + + HashMapFree(state_keytype); + Free(key_type_id); + return false; + } + /* Whenever event is valid or not really doesn't matter, as we're + * not using it's value anywhere. */ + HashMapSet(state_keytype, key_type_id, event); + Free(key_type_id); + + /* Step 2.2: If there are entries whose type and state_key don't + * match those specified by the auth events selection algorithm + * described in the server specification, reject. */ + if (!ValidAuthEventV1(&auth_pdu, &pdu)) + { + return false; + } + /* Step 2.3: If there are entries which were themselves rejected + * under the checks performed on receipt of a PDU, reject. + * TODO */ + if (!VerifyPDUV1(&auth_pdu)) + { + return false; + } + + /* Step 2.4: If there is no m.room.create event among the entries, + * reject. */ + + HashMapFree(event); + PduV1Free(&auth_pdu); + } + HashMapFree(state_keytype); + return true; +} +static bool +VerifyServers(char *id1, char *id2) +{ + CommonID cid1; + CommonID cid2; + + char *str1; + char *str2; + + bool ret = false; + + if (!ParseCommonID(id1, &cid1)) + { + return false; + } + if (!ParseCommonID(id2, &cid2)) + { + return false; + } + str1 = ParserRecomposeServerPart(cid1.server); + str2 = ParserRecomposeServerPart(cid2.server); + + if (StrEquals(str1, str2)) + { + ret = true; + goto end; + } +end: + Free(str1); + Free(str2); + CommonIDFree(cid1); + CommonIDFree(cid2); + return ret; +} +static bool +AuthoriseAliasV1(PduV1 pdu) +{ + /* Step 4.1: If event has no state_key, reject. */ + if (!pdu.state_key || StrEquals(pdu.state_key, "")) + { + return false; + } + /* Step 4.2: If sender's domain doesn't matches state_key, reject. */ + if (!VerifyServers(pdu.state_key, pdu.sender)) + { + return false; + } + + /* Step 4.3: Otherwise, allow. */ + return true; +} +static bool +AuthorizeJoinMembershipV1(Room * room, PduV1 pdu) +{ + /* Step 5.2.1: If the only previous event is an m.room.create and the + * state_key is the creator, allow. */ + Array *prev = pdu.prev_events; + if (ArraySize(prev) == 1) + { + char *prev_id = ArrayGet(prev, 0); + char *ignored; + HashMap *prev = RoomEventFetch(room, prev_id); + PduV1 prev_pdu; + + if (prev && PduV1FromJson(prev, &prev_pdu, &ignored)) + { + if (StrEquals(prev_pdu.type, "m.room.create") && + StrEquals(prev_pdu.sender, pdu.state_key)) + { + PduV1Free(&prev_pdu); + JsonFree(prev); + return true; + } + PduV1Free(&prev_pdu); + } + JsonFree(prev); + } + + /* Step 5.2.2: If the sender does not match state_key, reject. */ + if (!StrEquals(pdu.sender, pdu.state_key)) + { + return false; + } + /* Step 5.2.3: If the sender is banned, reject. */ + /* TODO */ + + /* Step 5.2.4: If the join_rule is invite then allow if membership + * state is invite or join. */ + /* Step 5.2.6: Otherwise, reject. */ + return false; +} +static bool +AuthoriseMemberV1(Room * room, PduV1 pdu) +{ + JsonValue *membership; + char *membership_str; + /* Step 5.1: If there is no state_key property, or no membership + * property in content, reject. */ + if (!pdu.state_key || + StrEquals(pdu.state_key, "") || + !(membership = JsonGet(pdu.content, 1, "membership"))) + { + return false; + } + if (JsonValueType(membership) != JSON_STRING) + { + /* Also check for the type */ + return false; + } + membership_str = JsonValueAsString(membership); +#define JumpIfMembership(mem, func) do { \ + if (StrEquals(membership_str, mem)) \ + { \ + return func(room, pdu); \ + } \ + } while (0) + + /* Step 5.2: If membership is join. */ + JumpIfMembership("join", AuthorizeJoinMembershipV1); + + /* Step 4.3: Otherwise, allow. */ + return true; +#undef JumpIfMembership +} +bool +AuthoriseEventV1(Room * room, PduV1 pdu) +{ + HashMap *state, *create_event; + char *create_event_id; + JsonValue *federate; + /* Step 1: If m.room.create */ if (StrEquals(pdu.type, "m.room.create")) { return AuthoriseCreateV1(pdu); } - /* TODO: Consider, everything else! */ - for (i = 0; i < ArraySize(pdu.auth_events); i++) + /* Step 2: Considering the event's auth_events. */ + if (!ConsiderAuthEventsV1(room, pdu)) { - char *event_id = ArrayGet(pdu.auth_events, i); - /* TODO: Fetch event there */ + return false; } - /* TODO */ + /* Step 3: If the content of the m.room.create event in the room state + * has the property m.federate set to false, and the sender domain of + * the event does not match the sender domain of the create event, + * reject. + */ + state = RoomStateGet(room); + create_event_id = StateGet(state, "m.room.create", ""); + if (!state || !create_event_id) + { + /* At this point, [create_event_id] has to exist */ + StateFree(state); /* create_event_id is also freed. */ + + return false; + } + create_event = RoomEventFetch(room, create_event_id); + federate = JsonGet(create_event, 2, "content", "m.federate"); + if (JsonValueType(federate) == JSON_BOOLEAN) + { + if (!JsonValueAsBoolean(federate)) + { + char *c_sender = + JsonValueAsString(JsonGet(create_event, 1, "sender")); + char *p_sender = pdu.sender; + if (!VerifyServers(c_sender, p_sender)) + { + return false; + } + } + } + + /* Step 4: If type is m.room.aliases */ + if (StrEquals(pdu.type, "m.room.aliases")) + { + return AuthoriseAliasV1(pdu); + } + + /* Step 5: If type is m.room.member */ + if (StrEquals(pdu.type, "m.room.member")) + { + return AuthoriseMemberV1(room, pdu); + } return true; } static char * @@ -357,7 +625,6 @@ RoomEventSendV1(Room * room, HashMap * event) #undef AddState } /* TODO: Do a size check! */ -finish: if (state) { JsonFree(state); @@ -373,6 +640,8 @@ static HashMap * RoomEventSendV3(Room * room, HashMap * event) { /* TODO */ + (void) room; + (void) event; return NULL; } HashMap * @@ -382,7 +651,7 @@ RoomEventSend(Room * room, HashMap * event) { return NULL; } - if (room->version == 1) + if (room->version < 3) { /* Manage with PDUv1 */ return RoomEventSendV1(room, event); @@ -390,3 +659,51 @@ RoomEventSend(Room * room, HashMap * event) /* Manage with PDUv3 otherwise */ return RoomEventSendV3(room, event); } + +static char * +CreateSafeID(char *unsafe_id) +{ + size_t length = strlen(unsafe_id); + char *safe_id = Malloc(length + 1); + size_t i; + /* Creates a room ID safe to be put into the database + * for room version 3 and above. + * (with '/'s replaced by '-') */ + memcpy(safe_id, unsafe_id, length + 1); + for (i = 0; i < length; i++) + { + if (safe_id[i] == '/') + { + safe_id[i] = '-'; + } + } + + return safe_id; +} +HashMap * +RoomEventFetch(Room *room, char *id) +{ + DbRef *event_ref; + HashMap *ret; + char *safe_id; + + if (!room || !id) + { + return NULL; + } + + /* Let's try to locally find that event in our junk. */ + safe_id = CreateSafeID(id); + event_ref = DbLock(room->db, 4, "rooms", room->id, "events", safe_id); + if (!event_ref) + { + /* TODO: Fetch from another homeserver if possible. */ + ret = NULL; + goto finish; + } + ret = JsonDuplicate(DbJson(event_ref)); + DbUnlock(room->db, event_ref); +finish: + Free(safe_id); + return ret; +} diff --git a/src/State.c b/src/State.c index 71d4cc9..632d0b2 100644 --- a/src/State.c +++ b/src/State.c @@ -26,10 +26,12 @@ #include #include +#include #include +#include -#include #include +#include static HashMap * StateResolveV1(Array * states) @@ -86,3 +88,35 @@ StateResolve(Room * room, HashMap * event) return StateResolveV2(states); } } +char * +StateGet(HashMap *state, char *type, char *key) +{ + char *full_string; + char *ret; + if (!state || !type || !key) + { + return NULL; + } + + full_string = StrConcat(3, type, ",", key); + ret = HashMapGet(state, full_string); + Free(full_string); + + return ret; +} +void +StateFree(HashMap *state) +{ + char *full; + char *event_id; + + if (!state) + { + return; + } + while (HashMapIterate(state, &full, (void **) &event_id)) + { + Free(event_id); + } + HashMapFree(state); +} diff --git a/src/include/Room.h b/src/include/Room.h index 0ec6b20..1b5ed8e 100644 --- a/src/include/Room.h +++ b/src/include/Room.h @@ -130,4 +130,12 @@ extern int RoomPrevEventsSet(Room *, Array *); */ extern HashMap * RoomEventSend(Room *, HashMap *); +/** + * Fetch a single event's PDU in a room into an + * hashmap, given an event ID, from the database + * if possible, or otherwise fetched from a remote + * homeserver participating in the room. + */ +extern HashMap * RoomEventFetch(Room *, char *); + #endif /* TELODENDRIA_ROOM_H */ diff --git a/src/include/State.h b/src/include/State.h index 1f5dd13..765caf4 100644 --- a/src/include/State.h +++ b/src/include/State.h @@ -54,4 +54,9 @@ extern char *StateSet(HashMap *, char *, char *, char *); */ extern HashMap * StateResolve(Room *, HashMap *); +/** + * Frees an entire state table from the heap. + */ +extern void StateFree(HashMap *); + #endif /* TELODENDRIA_STATE_H */