diff --git a/Schema/KeyUpload.json b/Schema/KeyUpload.json new file mode 100644 index 0000000..a0bafbd --- /dev/null +++ b/Schema/KeyUpload.json @@ -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 + } + } + } + } +} diff --git a/Schema/PduV1.json b/Schema/PduV1.json index d63f2df..0a45184 100644 --- a/Schema/PduV1.json +++ b/Schema/PduV1.json @@ -38,6 +38,10 @@ "redacted_because": { "type": "object", "required": false + }, + "transaction_id": { + "type": "string", + "required": false } } }, diff --git a/Schema/SyncResponse.json b/Schema/SyncResponse.json index c074f00..f9266fa 100644 --- a/Schema/SyncResponse.json +++ b/Schema/SyncResponse.json @@ -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" diff --git a/src/Room.c b/src/Room.c index e8670ca..fe62944 100644 --- a/src/Room.c +++ b/src/Room.c @@ -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 diff --git a/src/Room/Populate.c b/src/Room/Populate.c index 4d646fb..527aa32 100644 --- a/src/Room/Populate.c +++ b/src/Room/Populate.c @@ -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); diff --git a/src/Room/V1/Populate.c b/src/Room/V1/Populate.c index 54ea282..87d0afb 100644 --- a/src/Room/V1/Populate.c +++ b/src/Room/V1/Populate.c @@ -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 = diff --git a/src/Room/V1/Send.c b/src/Room/V1/Send.c index 2bf6f4b..b5b4dca 100644 --- a/src/Room/V1/Send.c +++ b/src/Room/V1/Send.c @@ -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); diff --git a/src/Routes/RouteActRoom.c b/src/Routes/RouteActRoom.c index 173dcd1..4d292a9 100644 --- a/src/Routes/RouteActRoom.c +++ b/src/Routes/RouteActRoom.c @@ -298,7 +298,7 @@ ROUTE_IMPL(RouteKickRoom, path, argp) membership = RoomEventCreate( sender, "m.room.member", kicked, - content + content, NULL ); HashMapSet(content, "membership", JsonValueString(membershipState)); diff --git a/src/Routes/RouteKeyManagement.c b/src/Routes/RouteKeyManagement.c index c095204..fdc0302 100644 --- a/src/Routes/RouteKeyManagement.c +++ b/src/Routes/RouteKeyManagement.c @@ -34,8 +34,105 @@ #include #include -#include +#include +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; } diff --git a/src/Routes/RouteSendEvent.c b/src/Routes/RouteSendEvent.c index e515d21..efadb08 100644 --- a/src/Routes/RouteSendEvent.c +++ b/src/Routes/RouteSendEvent.c @@ -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); diff --git a/src/Routes/RouteSync.c b/src/Routes/RouteSync.c index 3343078..1ca54df 100644 --- a/src/Routes/RouteSync.c +++ b/src/Routes/RouteSync.c @@ -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); diff --git a/src/Routes/RouteUserProfile.c b/src/Routes/RouteUserProfile.c index 9e144da..5c1a030 100644 --- a/src/Routes/RouteUserProfile.c +++ b/src/Routes/RouteUserProfile.c @@ -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")); diff --git a/src/User.c b/src/User.c index ffef0c9..6248160 100644 --- a/src/User.c +++ b/src/User.c @@ -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; +} diff --git a/src/include/Room.h b/src/include/Room.h index 7d7e53b..d7151a1 100644 --- a/src/include/Room.h +++ b/src/include/Room.h @@ -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 diff --git a/src/include/User.h b/src/include/User.h index 27007f7..07430fa 100644 --- a/src/include/User.h +++ b/src/include/User.h @@ -44,6 +44,8 @@ #include +#include + #include /** @@ -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 */