[ADD/WIP] Dumb key management

This is just enough to fool FluffyChat and nheko, and still not enough
to actually do proper E2EE. Also, don't use Cinny with it, it seems to
repeteadly upload keys. Terrible.
This commit is contained in:
LDA 2024-12-02 11:50:38 +01:00
parent 8612aae24c
commit 4b427a4c82
15 changed files with 405 additions and 28 deletions

62
Schema/KeyUpload.json Normal file
View file

@ -0,0 +1,62 @@
{
"guard": "TELODENDRIA_KEY_UPLOAD_H",
"header": "Schema/KeyUpload.h",
"types": {
"DeviceKeys": {
"type": "struct",
"fields": {
"device_id": {
"type": "string",
"required": true
},
"user_id": {
"type": "string",
"required": true
},
"algorithms": {
"type": "[string]",
"required": true
},
"keys": {
"type": "{string}",
"required": true
},
"signatures": {
"//": "TODO: More complex j2s types.",
"//": "This is meant to be a map from user ID to ",
"//": "algo+device ID to a signature(string).",
"type": "object",
"required": true
}
}
},
"KeyResponse": {
"type": "struct",
"fields": {
"one_time_key_counts": {
"type": "{integer}",
"required": true
}
}
},
"KeyUploadRequest": {
"type": "struct",
"fields": {
"device_keys": {
"type": "DeviceKeys",
"required": false
},
"fallback_keys": {
"//": "This is a one-time key.",
"type": "object",
"required": false
},
"one_time_keys": {
"//": "This is a one-time key.",
"type": "object",
"required": false
}
}
}
}
}

View file

@ -38,6 +38,10 @@
"redacted_because": {
"type": "object",
"required": false
},
"transaction_id": {
"type": "string",
"required": false
}
}
},

View file

@ -68,6 +68,7 @@
"sender": { "type": "string", "required": true },
"state_key": { "type": "string" },
"redacts": { "type": "string" },
"_unsigned": { "type": "object" },
"type": { "type": "string", "required": true }
},
"type": "struct"
@ -105,6 +106,9 @@
},
"rooms": {
"type": "Rooms"
},
"device_one_time_keys_count": {
"type": "{integer}"
}
},
"type": "struct"

View file

@ -348,6 +348,11 @@ RoomEventFetch(Room *room, char *id, bool prev)
"pdu_status",
JsonValueDuplicate(HashMapGet(DbJson(event_ref), "status"))
));
JsonValueFree(HashMapSet(
unsign,
"transaction_id",
JsonValueDuplicate(HashMapGet(DbJson(event_ref), "transaction"))
));
ts = JsonValueAsInteger(HashMapGet(ret, "origin_server_ts"));
JsonValueFree(HashMapSet(
unsign,
@ -391,7 +396,7 @@ finish:
}
HashMap *
RoomEventCreate(char *sender, char *type, char *key, HashMap *c)
RoomEventCreate(char *sender, char *type, char *key, HashMap *c, char *txn)
{
HashMap *event;
if (!sender || !type || !c)
@ -403,6 +408,7 @@ RoomEventCreate(char *sender, char *type, char *key, HashMap *c)
JsonSet(event, JsonValueObject(c), 1, "content");
JsonSet(event, JsonValueString(sender), 1, "sender");
JsonSet(event, JsonValueString(type), 1, "type");
JsonSet(event, JsonValueString(txn), 1, "transaction");
if (key)
{
JsonSet(event, JsonValueString(key), 1, "state_key");
@ -557,7 +563,7 @@ RoomSendInvite(User *sender, bool direct, char *user, Room *room)
content = HashMapCreate();
JsonSet(content, JsonValueBoolean(direct), 1, "is_direct");
JsonSet(content, JsonValueString("invite"), 1, "membership");
event = RoomEventCreate(senderStr, "m.room.member", user, content);
event = RoomEventCreate(senderStr, "m.room.member", user, content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
@ -692,7 +698,7 @@ RoomLeave(Room *room, User *user, char **errp)
content = HashMapCreate();
JsonSet(content, JsonValueString("leave"), 1, "membership");
event = RoomEventCreate(userString, "m.room.member", userString, content);
event = RoomEventCreate(userString, "m.room.member", userString, content, NULL);
pdu = RoomEventSend(room, event, errp);
/* TODO: One ought to be extremely careful with managing users in those
@ -756,7 +762,7 @@ RoomRedact(Room *room, User *user, char *eventID, char *reason, char **errp)
HashMapSet(content, "reason", JsonValueString(reason));
event = RoomEventCreate(userString,
"m.room.redaction", NULL,
content
content, NULL
);
HashMapSet(event, "redacts", JsonValueString(eventID));
pdu = RoomEventSend(room, event, errp);
@ -811,7 +817,7 @@ RoomJoin(Room *room, User *user, char **errp)
content = HashMapCreate();
JsonSet(content, JsonValueString("join"), 1, "membership");
event = RoomEventCreate(userString, "m.room.member", userString, content);
event = RoomEventCreate(userString, "m.room.member", userString, content, NULL);
pdu = RoomEventSend(room, event, errp);
/* TODO: One ought to be extremely careful with managing users in those

View file

@ -97,7 +97,7 @@ RoomPopulate(Room *room, User *user, RoomCreateRequest *req, ServerPart s)
}
JsonValueFree(HashMapSet(content, key, JsonValueDuplicate(val)));
}
event = RoomEventCreate(sender_str, "m.room.create", "", content);
event = RoomEventCreate(sender_str, "m.room.create", "", content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
UserAddJoin(user, room->id);
@ -105,7 +105,7 @@ RoomPopulate(Room *room, User *user, RoomCreateRequest *req, ServerPart s)
/* m.room.member */
content = HashMapCreate();
JsonSet(content, JsonValueString("join"), 1, "membership");
event = RoomEventCreate(sender_str, "m.room.member", sender_str, content);
event = RoomEventCreate(sender_str, "m.room.member", sender_str, content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
@ -126,7 +126,7 @@ RoomPopulate(Room *room, User *user, RoomCreateRequest *req, ServerPart s)
}
HashMapSet(content, key, JsonValueDuplicate(val));
}
event = RoomEventCreate(sender_str, "m.room.power_levels", "", content);
event = RoomEventCreate(sender_str, "m.room.power_levels", "", content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
@ -174,7 +174,7 @@ RoomPopulate(Room *room, User *user, RoomCreateRequest *req, ServerPart s)
, 1, a); \
event = RoomEventCreate( \
sender_str, \
"m.room." #p, "", content); \
"m.room." #p, "", content, NULL); \
JsonFree(RoomEventSend(room, event, NULL)); \
JsonFree(event); \
} \
@ -208,7 +208,7 @@ RoomPopulate(Room *room, User *user, RoomCreateRequest *req, ServerPart s)
{
content = HashMapCreate();
JsonSet(content, JsonValueString(req->name), 1, "name");
event = RoomEventCreate(sender_str, "m.room.name", "", content);
event = RoomEventCreate(sender_str, "m.room.name", "", content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
}
@ -216,7 +216,7 @@ RoomPopulate(Room *room, User *user, RoomCreateRequest *req, ServerPart s)
{
content = HashMapCreate();
JsonSet(content, JsonValueString(req->topic), 1, "topic");
event = RoomEventCreate(sender_str, "m.room.topic", "", content);
event = RoomEventCreate(sender_str, "m.room.topic", "", content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
}
@ -240,7 +240,7 @@ RoomPopulate(Room *room, User *user, RoomCreateRequest *req, ServerPart s)
JsonSet(content, JsonValueString(fullStr), 1, "alias");
event = RoomEventCreate(
sender_str,
"m.room.canonical_alias", "", content);
"m.room.canonical_alias", "", content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
@ -270,7 +270,7 @@ RoomPopulate(Room *room, User *user, RoomCreateRequest *req, ServerPart s)
}
event = RoomEventCreate(sender_str, "m.room.power_levels", "", pl_content);
event = RoomEventCreate(sender_str, "m.room.power_levels", "", pl_content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);

View file

@ -29,6 +29,8 @@ PopulateEventV1(Room * room, HashMap * event, PduV1 * pdu, ServerPart serv)
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "type")));
pdu->redacts =
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "redacts")));
pdu->_unsigned.transaction_id =
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "transaction")));
if (JsonGet(event, 1, "state_key"))
{
pdu->state_key =

View file

@ -242,6 +242,11 @@ RoomAddEventV1(Room *room, PduV1 pdu, PduV1Status status)
JsonValueArray(ArrayCreate()),
1, "next_events"
);
JsonSet(
DbJson(event_ref),
JsonValueString(pdu._unsigned.transaction_id),
1, "transaction"
);
DbUnlock(room->db, event_ref);
Free(safe_id);

View file

@ -298,7 +298,7 @@ ROUTE_IMPL(RouteKickRoom, path, argp)
membership = RoomEventCreate(
sender,
"m.room.member", kicked,
content
content, NULL
);
HashMapSet(content, "membership", JsonValueString(membershipState));

View file

@ -34,8 +34,105 @@
#include <Room.h>
#include <string.h>
#include <Schema/Filter.h>
#include <Schema/KeyUpload.h>
HashMap *
UploadKey(RouteArgs *args, User *user, KeyUploadRequest *req, char *sender)
{
char *deviceId = UserGetDeviceId(user);
KeyResponse response = { 0 };
HashMap *json;
char *fbKey;
JsonValue *fbValue;
size_t i;
if (!user || !req || !sender)
{
return NULL;
}
Log(LOG_ERR, "did=%s", deviceId);
if (req->device_keys.user_id)
{
HashMap *publicKeys;
char *pkTag, *pk;
/* We have device key information */
if (!StrEquals(req->device_keys.user_id, sender))
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(
M_UNAUTHORIZED, "Device key update has an invalid user ID"
);
}
if (!StrEquals(req->device_keys.device_id, deviceId))
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(
M_UNAUTHORIZED, "Device key update has an invalid device ID"
);
}
/* Check the public key list */
publicKeys = req->device_keys.keys;
i = 0;
while (HashMapIterateReentrant(publicKeys, &pkTag, (void **) &pk, &i))
{
char *pktDID = strchr(pkTag, ':');
/* Maybe C does need NULL saturation */
pktDID = pktDID ? pktDID + 1 : NULL;
if (!StrEquals(pktDID, deviceId))
{
/* As far as I know, we're not meant to handle other devices'
* public keys */
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
Log(LOG_ERR, "%s!=%s 1", pktDID, deviceId);
return MatrixErrorCreate(
M_UNAUTHORIZED, "Device key update has an invalid device ID"
);
}
}
UserSetDeviceKeys(user, &req->device_keys);
}
UserClearFallbackKeys(user);
i = 0;
while (HashMapIterateReentrant(req->fallback_keys, &fbKey, (void **) &fbValue, &i))
{
char *fbKID = strchr(fbKey, ':');
size_t len = fbKID ? fbKID - fbKey : 0;
char algo[len + 1];
memcpy(algo, fbKey, len);
algo[len] = '\0';
/* Maybe C does need NULL saturation */
fbKID = fbKID ? fbKID + 1 : NULL;
UserAddKey(user, fbKey, fbValue, true);
(void) fbKID;
}
i = 0;
while (HashMapIterateReentrant(req->one_time_keys, &fbKey, (void **) &fbValue, &i))
{
char *fbKID = strchr(fbKey, ':');
size_t len = fbKID ? fbKID - fbKey : 0;
char algo[len + 1];
memcpy(algo, fbKey, len);
algo[len] = '\0';
/* Maybe C does need NULL saturation */
fbKID = fbKID ? fbKID + 1 : NULL;
UserAddKey(user, fbKey, fbValue, false);
(void) fbKID;
}
response.one_time_key_counts = UserGetOnetimeCounts(user);
UserNotifyUser(UserGetName(user));
json = KeyResponseToJson(&response);
KeyResponseFree(&response);
return json;
}
ROUTE_IMPL(RouteKeyQuery, path, argp)
{
@ -45,9 +142,14 @@ ROUTE_IMPL(RouteKeyQuery, path, argp)
HashMap *request = NULL;
HashMap *response = NULL;
User *user = NULL;
CommonID *id = NULL;
char *token = NULL;
User *user = NULL;
char *serverName = NULL;
char *sender = NULL;
char *method = ArrayGet(path, 0);
char *err;
if (HttpRequestMethodGet(args->context) != HTTP_POST)
@ -71,6 +173,11 @@ ROUTE_IMPL(RouteKeyQuery, path, argp)
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
serverName = ConfigGetServerName(db);
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
@ -79,10 +186,37 @@ ROUTE_IMPL(RouteKeyQuery, path, argp)
goto finish;
}
if (StrEquals(method, "upload"))
{
KeyUploadRequest upload = { 0 };
if (!KeyUploadRequestFromJson(request, &upload, &err))
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, err);
goto finish;
}
if ((response = UploadKey(args, user, &upload, sender)))
{
KeyUploadRequestFree(&upload);
goto finish;
}
KeyUploadRequestFree(&upload);
}
else if (StrEquals(method, "query"))
{
/* TODO: Fetch a user's key information */
}
response = HashMapCreate();
(void) path;
finish:
JsonFree(request);
UserUnlock(user);
Free(serverName);
UserIdFree(id);
Free(sender);
return response;
}

View file

@ -122,7 +122,7 @@ ROUTE_IMPL(RouteSendEvent, path, argp)
goto finish;
}
event = RoomEventCreate(sender, eventType, NULL, JsonDuplicate(request));
event = RoomEventCreate(sender, eventType, NULL, JsonDuplicate(request), transId);
filled = RoomEventSend(room, event, &err);
JsonFree(event);
@ -266,7 +266,7 @@ ROUTE_IMPL(RouteSendState, path, argp)
event = RoomEventCreate(
sender,
eventType, stateKey ? stateKey : "",
JsonDuplicate(request)
JsonDuplicate(request), NULL
);
filled = RoomEventSend(room, event, &err);
JsonFree(event);

View file

@ -139,12 +139,17 @@ ROUTE_IMPL(RouteSync, path, argp)
/* TODO: I only am manually parsing this because j2s does not support
* a hashmap of unknown keys pointing to a known type. */
sync.rooms.invite = HashMapCreate();
sync.rooms.join = HashMapCreate();
sync.account_data.events = ArrayCreate();
sync.rooms.invite = NULL;
sync.rooms.join = NULL;
sync.account_data.events = NULL;
sync.device_one_time_keys_count = UserGetOnetimeCounts(user);
/* account data */
accountData = UserGetAccountDataSync(user, currBatch);
if (ArraySize(accountData) > 0)
{
sync.account_data.events = ArrayCreate();
}
for (i = 0; i < ArraySize(accountData); i++)
{
char *key = ArrayGet(accountData, i);
@ -158,6 +163,10 @@ ROUTE_IMPL(RouteSync, path, argp)
/* invites */
invites = UserGetInvites(user, currBatch);
if (ArraySize(invites) > 0)
{
sync.rooms.invite = HashMapCreate();
}
for (i = 0; i < ArraySize(invites); i++)
{
char *roomId = ArrayGet(invites, i);
@ -171,7 +180,7 @@ ROUTE_IMPL(RouteSync, path, argp)
invited = Malloc(sizeof(*invited));
memset(invited, 0, sizeof(*invited));
/* TODO: Populate the invitestate */
// TODO: Populate the invitestate
invited->invite_state.events = ArrayCreate();
HashMapSet(sync.rooms.invite, roomId, invited);
}
@ -179,9 +188,12 @@ ROUTE_IMPL(RouteSync, path, argp)
/* Joins */
joins = UserGetJoins(user, currBatch);
if (ArraySize(joins) > 0)
{
sync.rooms.join = HashMapCreate();
}
for (i = 0; i < ArraySize(joins); i++)
{
/* TODO: Rename these variables */
char *roomId = ArrayGet(joins, i);
JoinedRooms *joined;
char *firstEvent = NULL;
@ -230,8 +242,8 @@ ROUTE_IMPL(RouteSync, path, argp)
joined->timeline.prev_batch = UserNewMessageToken(
user, roomId, firstEvent
);
/* TODO: Don't shove the entire state.
* That's a recipe for disaster, especially on large rooms. */
// TODO: Don't shove the entire state.
// That's a recipe for disaster, especially on large rooms.
joined->state.events = ArrayCreate();
while (StateIterate(state, &type, &key, (void **) &id))
{
@ -253,12 +265,13 @@ ROUTE_IMPL(RouteSync, path, argp)
if (prevBatch)
{
/* TODO: Should we be dropping syncs? */
UserDropSync(user, prevBatch);
//UserDropSync(user, prevBatch);
nextBatch = UserInitSyncDiff(user);
}
sync.next_batch = nextBatch;
response = SyncResponseToJson(&sync);
SyncResponseFree(&sync);
(void) i;
finish:
FilterDestroy(filterData);
UserUnlock(user);

View file

@ -56,7 +56,7 @@ SendMembership(Db *db, User *user)
HashMap *content = HashMapCreate();
HashMap *membership = RoomEventCreate(
sender, "m.room.member", sender,
content
content, NULL
);
HashMapSet(content, "membership", JsonValueString("join"));

View file

@ -1533,6 +1533,7 @@ UserGetAccountDataSync(User *user, char *batch)
syncRef = DbLock(db, 4, "users", user->name, "sync", batch);
if (!syncRef)
{
Log(LOG_ERR, "Tried to get batch=%s (user=%s), but it's gone?", batch, user->deviceId);
return NULL;
}
@ -2153,3 +2154,125 @@ UserSetAccountData(User *user, char *key, HashMap *obj)
UserPushAccountData(user, key);
}
void
UserSetDeviceKeys(User *user, DeviceKeys *keys)
{
char *device;
HashMap *deviceObj;
if (!user || !keys)
{
return;
}
device = UserGetDeviceId(user);
deviceObj = JsonValueAsObject(HashMapGet(
UserGetDevices(user), device
));
if (!deviceObj)
{
return;
}
JsonValueFree(HashMapSet(deviceObj, "deviceKeys", JsonValueObject(DeviceKeysToJson(keys))));
}
void
UserClearFallbackKeys(User *user)
{
char *device;
HashMap *deviceObj;
if (!user)
{
return;
}
device = UserGetDeviceId(user);
deviceObj = JsonValueAsObject(HashMapGet(
UserGetDevices(user), device
));
if (!deviceObj)
{
return;
}
if (!HashMapGet(deviceObj, "oneTimeKeys"))
{
JsonValueFree(HashMapSet(deviceObj,
"oneTimeKeys", JsonValueObject(HashMapCreate())
));
}
JsonValueFree(HashMapSet(deviceObj,
"fallbackKeys", JsonValueObject(HashMapCreate())
));
}
void
UserAddKey(User *user, char *algo, JsonValue *key, bool fb)
{
char *device;
HashMap *deviceObj, *method;
if (!user || !algo || !key)
{
return;
}
device = UserGetDeviceId(user);
deviceObj = JsonValueAsObject(HashMapGet(
UserGetDevices(user), device
));
if (!deviceObj)
{
return;
}
method = JsonValueAsObject(HashMapGet(
deviceObj, fb ? "fallbackKeys" : "oneTimeKeys"
));
JsonValueFree(HashMapSet(method, algo, JsonValueDuplicate(key)));
}
HashMap *
UserGetOnetimeCounts(User *user)
{
char *device, *algoKey;
HashMap *deviceObj, *otk, *ret;
void *ignore;
if (!user)
{
return NULL;
}
device = UserGetDeviceId(user);
deviceObj = JsonValueAsObject(HashMapGet(
UserGetDevices(user), device
));
if (!deviceObj)
{
return NULL;
}
otk = JsonValueAsObject(HashMapGet(
deviceObj, "oneTimeKeys"
));
ret = HashMapCreate();
while (HashMapIterate(otk, &algoKey, &ignore))
{
char *algo = StrDuplicate(algoKey);
char *end = strchr(algo, ':');
int64_t *ptr;
if (end)
{
*end = '\0';
}
if (!(ptr = HashMapGet(ret, algo)))
{
ptr = Malloc(sizeof(*ptr));
*ptr = 0;
HashMapSet(ret, algo, ptr);
}
(*ptr)++;
Free(algo);
}
return ret;
}

View file

@ -195,7 +195,7 @@ extern bool RoomAddEventV1(Room *, PduV1, PduV1Status);
* Creates a barebones JSON object to be sent to
* .Fn RoomEventFetch .
*/
extern HashMap * RoomEventCreate(char *, char *, char *, HashMap *);
extern HashMap * RoomEventCreate(char *, char *, char *, HashMap *, char *);
/**
* Computes an approximation of the PDU depth by looking at

View file

@ -44,6 +44,8 @@
#include <Parser.h>
#include <Schema/KeyUpload.h>
#include <stdbool.h>
/**
@ -522,4 +524,26 @@ extern HashMap * UserGetAccountData(User *, char *);
* Replaces an account data entry.
*/
extern void UserSetAccountData(User *, char *, HashMap *);
/**
* Sets the device key list.
*/
extern void UserSetDeviceKeys(User *, DeviceKeys *);
/**
* Clears the fallback/one-time key list.
*/
extern void UserClearFallbackKeys(User *);
/**
* Adds a one-time/fallback key.
*/
extern void UserAddKey(User *, char *, JsonValue *, bool);
/**
* Generates a hashmap from algorithm to one-time key count as
* a pointer to the uint64_t.
* This is intended for /keys/upload. Please do not use this
* elsewhere */
extern HashMap * UserGetOnetimeCounts(User *);
#endif /* TELODENDRIA_USER_H */