#include "../include/dchat/Room.hpp" #include "../include/dchat/Storage.hpp" #include "../include/dchat/FileUtil.hpp" #include "../include/dchat/RoomDataType.hpp" #include #include #include #include #include #include #include // TODO: Remove error checks when odhtdb has been improved to take care of such errors namespace dchat { Room::Room(Rooms *_rooms, std::shared_ptr _id) : rooms(_rooms), id(_id), userdata(nullptr) { } void Room::addUser(const odhtdb::Signature::PublicKey &userPublicKey, std::shared_ptr group) { assert(localUser); assert(publicKeyToKeyPairMap.find(localUser->publicKey) != publicKeyToKeyPairMap.end()); assert(encryptionKey); auto keyPair = publicKeyToKeyPairMap[localUser->publicKey]; odhtdb::DatabaseNode roomNode(encryptionKey, id); rooms->database->addUser(roomNode, *keyPair, userPublicKey, odhtdb::DataView(group->id, odhtdb::GROUP_ID_LENGTH)); } std::shared_ptr Room::addUserLocally(const odhtdb::Signature::PublicKey &userPublicKey, std::shared_ptr group) { auto existingUser = getUserByPublicKey(userPublicKey); if(existingUser) { odhtdb::Log::error("The user %s already exists in the room %s", userPublicKey.toString().c_str(), id->toString().c_str()); assert(false); return nullptr; } auto user = std::make_shared(userPublicKey); user->groups.push_back(group); userByPublicKey[userPublicKey] = user; return user; } std::shared_ptr Room::addGroupLocally(const odhtdb::DataView groupId) { auto existingGroup = getGroupById(groupId); if(existingGroup) { odhtdb::Log::error("The group %s already exists in the room %s", groupId.toString().c_str(), id->toString().c_str()); assert(false); return nullptr; } auto group = std::make_shared(); assert(groupId.size == odhtdb::GROUP_ID_LENGTH); memcpy(group->id, groupId.data, groupId.size); groups.push_back(group); return group; } std::shared_ptr Room::getUserByPublicKey(const odhtdb::Signature::PublicKey &userPublicKey) { auto userIt = userByPublicKey.find(userPublicKey); if(userIt != userByPublicKey.end()) return userIt->second; return nullptr; } std::shared_ptr Room::getGroupById(const odhtdb::DataView groupId) { for(std::shared_ptr &group : groups) { if(odhtdb::DataView(group->id, odhtdb::GROUP_ID_LENGTH) == groupId) return group; } return nullptr; } static std::string getInviteKeyChecksum(const std::string &inviteKey) { u8 checksum = 0x3d; for(char c : inviteKey) { checksum ^= c; } return odhtdb::bin2hex((const char*)&checksum, 1); } void Room::setLocalUser(std::shared_ptr user, std::shared_ptr keyPair) { if(localUser) return; assert(user->publicKey == keyPair->getPublicKey()); assert(encryptionKey); localUser = user; publicKeyToKeyPairMap[user->publicKey] = keyPair; odhtdb::Log::debug("Local user set to %s for room %s", user->publicKey.toString().c_str(), id->toString().c_str()); // Room id + room encryption key + invite key checksum (to verify invite key is not typed incorrectly) inviteKey = id->toString() + odhtdb::bin2hex((const char*)encryptionKey->data, encryptionKey->size); inviteKey += getInviteKeyChecksum(inviteKey); odhtdb::InfoHash inviteInfoHash = odhtdb::InfoHash::generateHash((const u8*)inviteKey.data(), inviteKey.size()); // TODO: Use database instead of custom message. Then both parties dont have to be online to add user to room. // TODO: Only listen for invite request if we have the right to add users to the room. // TODO: Make this optional (should be possible to turn off from gui/settings) rooms->database->receiveCustomMessage(inviteInfoHash, [this](const void *data, usize size) { sibs::SafeSerializer serializer; try { odhtdb::Log::debug("Received request to invite user to room"); sibs::SafeDeserializer deserializer((const u8*)data, size); u8 nonce[odhtdb::ENCRYPTION_NONCE_BYTE_SIZE]; deserializer.extract(nonce, sizeof(nonce)); odhtdb::Decryption decryptedData( { (void*)deserializer.getBuffer(), deserializer.getSize() }, { nonce, sizeof(nonce) }, { (void*)encryptionKey->data, encryptionKey->size }); sibs::SafeDeserializer deserializerMsg((const u8*)decryptedData.getDecryptedText().data, decryptedData.getDecryptedText().size); odhtdb::Signature::PublicKey userPublicKey; deserializerMsg.extract((u8*)userPublicKey.getData(), userPublicKey.getSize()); auto user = getUserByPublicKey(userPublicKey); if(user) { odhtdb::Log::warn("User %s wanted us to add them to the room %s but they are already a member of it", userPublicKey.toString().c_str(), id->toString().c_str()); // TODO: Return error message to user, so they can get a nice error message (or deny message) return serializer; } std::string userMsg; if(!deserializerMsg.empty()) { userMsg.resize(deserializerMsg.getSize()); deserializerMsg.extract((u8*)&userMsg[0], deserializerMsg.getSize()); } if(rooms->callbackFuncs.receiveInviteUserCallbackFunc) { InviteUserRequest request; request.room = this; request.userPublicKey = std::move(userPublicKey); request.message = std::move(userMsg); rooms->callbackFuncs.receiveInviteUserCallbackFunc(request); } } catch(std::exception &e) { odhtdb::Log::warn("Failed to deserialize request to invite user to room"); } return serializer; }); } void Room::setAvatarUrl(const std::string &url) { assert(localUser); assert(publicKeyToKeyPairMap.find(localUser->publicKey) != publicKeyToKeyPairMap.end()); assert(encryptionKey); sibs::SafeSerializer serializer; serializer.add(RoomDataType::CHANGE_AVATAR); serializer.add((u16)url.size()); serializer.add(url.data(), url.size()); auto keyPair = publicKeyToKeyPairMap[localUser->publicKey]; odhtdb::DatabaseNode roomNode(encryptionKey, id); rooms->database->addData(roomNode, *keyPair, { serializer.getBuffer().data(), serializer.getSize() }); } void Room::setUserNickname(const std::string &nickname) { assert(localUser); assert(publicKeyToKeyPairMap.find(localUser->publicKey) != publicKeyToKeyPairMap.end()); assert(encryptionKey); sibs::SafeSerializer serializer; serializer.add(RoomDataType::NICKNAME_CHANGE); serializer.add((u8)nickname.size()); serializer.add(nickname.data(), nickname.size()); auto keyPair = publicKeyToKeyPairMap[localUser->publicKey]; odhtdb::DatabaseNode roomNode(encryptionKey, id); rooms->database->addData(roomNode, *keyPair, { serializer.getBuffer().data(), serializer.getSize() }); } bool Room::setName(const std::string &name) { assert(localUser); assert(encryptionKey); int userPermissionLevel = rooms->database->getUserLowestPermissionLevel(*id, localUser->publicKey); if(userPermissionLevel != odhtdb::PERMISSION_LEVEL_ADMIN) { odhtdb::Log::debug("Room change name: attempted by user %s who is not an admin (permission level: %d)\n", localUser->publicKey.toString().c_str(), userPermissionLevel); return false; } sibs::SafeSerializer serializer; serializer.add(RoomDataType::CHANGE_ROOM_NAME); serializer.add((u16)name.size()); serializer.add(name.data(), name.size()); auto keyPair = publicKeyToKeyPairMap[localUser->publicKey]; odhtdb::DatabaseNode roomNode(encryptionKey, id); rooms->database->addData(roomNode, *keyPair, { serializer.getBuffer().data(), serializer.getSize() }); return true; } void Room::publishMessage(const std::string &msg) { assert(localUser); assert(publicKeyToKeyPairMap.find(localUser->publicKey) != publicKeyToKeyPairMap.end()); assert(encryptionKey); sibs::SafeSerializer serializer; serializer.add(RoomDataType::ADD_MESSAGE); serializer.add(msg.data(), msg.size()); auto keyPair = publicKeyToKeyPairMap[localUser->publicKey]; odhtdb::DatabaseNode roomNode(encryptionKey, id); rooms->database->addData(roomNode, *keyPair, { serializer.getBuffer().data(), serializer.getSize() }); } Rooms::Rooms(const char *address, u16 port, RoomCallbackFuncs _callbackFuncs) : callbackFuncs(_callbackFuncs), loggedIn(false) { odhtdb::DatabaseCallbackFuncs databaseCallbackFuncs; databaseCallbackFuncs.createNodeCallbackFunc = std::bind(&Rooms::createNodeCallbackFunc, this, std::placeholders::_1); databaseCallbackFuncs.addNodeCallbackFunc = std::bind(&Rooms::addNodeCallbackFunc, this, std::placeholders::_1); databaseCallbackFuncs.addUserCallbackFunc = std::bind(&Rooms::addUserCallbackFunc, this, std::placeholders::_1); database = odhtdb::Database::connect(address, port, getDchatDir(), databaseCallbackFuncs).get(); } void Rooms::createNodeCallbackFunc(const odhtdb::DatabaseCreateNodeRequest &request) { std::lock_guard lock(roomModifyMutex); auto roomIt = roomById.find(*request.nodeHash); if(roomIt != roomById.end()) { odhtdb::Log::error("Room %s has already been created once", request.nodeHash->toString().c_str()); return; } auto roomId = std::make_shared(*request.nodeHash); auto room = std::make_shared(this, roomId); auto encryptionKeyIt = roomEncryptionKey.find(*request.nodeHash); if(encryptionKeyIt != roomEncryptionKey.end()) room->encryptionKey = encryptionKeyIt->second; roomById[*request.nodeHash] = room; if(callbackFuncs.createRoomCallbackFunc) callbackFuncs.createRoomCallbackFunc(room); auto group = room->addGroupLocally(request.groupId); auto user = room->addUserLocally(*request.creatorPublicKey, group); bool isLocalUser = false; if(!room->localUser) { auto localUserIt = roomLocalUser.find(*request.nodeHash); if(localUserIt != roomLocalUser.end() && user->publicKey == localUserIt->second->getPublicKey()) { room->setLocalUser(user, localUserIt->second); isLocalUser = true; } } if(callbackFuncs.addUserCallbackFunc) { RoomAddUserRequest callbackRequest; callbackRequest.room = room; callbackRequest.user = user; callbackRequest.addedByUser = user; callbackRequest.timestampSeconds = ntp::NtpTimestamp::fromCombined(request.timestamp).seconds; callbackRequest.loadedFromCache = request.loadedFromCache; callbackRequest.isLocalUser = isLocalUser; callbackRequest.waitedToJoin = false; callbackFuncs.addUserCallbackFunc(callbackRequest); } } void Rooms::addNodeCallbackFunc(const odhtdb::DatabaseAddNodeRequest &request) { std::lock_guard lock(roomModifyMutex); auto roomIt = roomById.find(*request.nodeHash); if(roomIt == roomById.end()) { odhtdb::Log::error("Attempting to add data to node %s but the node has not been created", request.nodeHash->toString().c_str()); return; } auto room = roomIt->second; auto user = room->getUserByPublicKey(*request.creatorPublicKey); if(!user) { odhtdb::Log::error("Attempting to add data to node %s but the the user %s doesn't exist in the node %s", request.creatorPublicKey->toString().c_str(), request.nodeHash->toString().c_str()); return; } if(request.decryptedData.size == 0) return; uint32_t timestampSeconds = ntp::NtpTimestamp::fromCombined(request.timestamp).seconds; RoomDataType roomDataType = (RoomDataType)static_cast(request.decryptedData.data)[0]; try { switch(roomDataType) { case RoomDataType::ADD_MESSAGE: { auto message = std::make_shared(); message->id = *request.requestHash; message->creator = user; message->timestampSeconds = timestampSeconds; message->text = std::string((const char*)request.decryptedData.data + 1, request.decryptedData.size - 1); RoomAddMessageRequest roomRequest; roomRequest.room = room; roomRequest.loadedFromCache = request.loadedFromCache; roomRequest.message = std::move(message); roomRequest.prevMessage = nullptr; if(!room->messages.empty()) roomRequest.prevMessage = room->messages.back(); if(callbackFuncs.addMessageCallbackFunc) callbackFuncs.addMessageCallbackFunc(roomRequest); room->messages.push_back(std::move(roomRequest.message)); break; } case RoomDataType::NICKNAME_CHANGE: { sibs::SafeDeserializer deserializer((const u8*)request.decryptedData.data + 1, request.decryptedData.size - 1); u8 nameLength = deserializer.extract(); if(nameLength > 0) { std::string nickname; nickname.resize(nameLength); deserializer.extract((u8*)&nickname[0], nameLength); UserChangeNicknameRequest roomRequest; roomRequest.room = room; roomRequest.user = user; roomRequest.timestampSeconds = timestampSeconds; roomRequest.loadedFromCache = request.loadedFromCache; roomRequest.newNickname = nickname; if(callbackFuncs.userChangeNicknameCallbackFunc) callbackFuncs.userChangeNicknameCallbackFunc(roomRequest); user->nickname = std::move(nickname); } break; } case RoomDataType::CHANGE_AVATAR: { sibs::SafeDeserializer deserializer((const u8*)request.decryptedData.data + 1, request.decryptedData.size - 1); u16 urlLength = deserializer.extract(); if(urlLength > 0) { std::string url; url.resize(urlLength); deserializer.extract((u8*)&url[0], urlLength); UserChangeAvatarRequest roomRequest; roomRequest.room = room; roomRequest.user = user; roomRequest.timestampSeconds = timestampSeconds; roomRequest.loadedFromCache = request.loadedFromCache; roomRequest.url = url; if(callbackFuncs.userChangeAvatarCallbackFunc) callbackFuncs.userChangeAvatarCallbackFunc(roomRequest); user->avatarUrl = std::move(url); } break; } case RoomDataType::CHANGE_ROOM_NAME: { int userPermissionLevel = database->getUserLowestPermissionLevel(*room->id, user->publicKey); if(userPermissionLevel != odhtdb::PERMISSION_LEVEL_ADMIN) { odhtdb::Log::debug("Room change name: attempted by user %s who is not an admin (permission level: %d)\n", user->publicKey.toString().c_str(), userPermissionLevel); return; } sibs::SafeDeserializer deserializer((const u8*)request.decryptedData.data + 1, request.decryptedData.size - 1); u16 channelNameLength = deserializer.extract(); if(channelNameLength > 0 && channelNameLength <= 32) { RoomChangeNameRequest roomRequest; roomRequest.room = room; roomRequest.user = user; roomRequest.timestampSeconds = timestampSeconds; roomRequest.loadedFromCache = request.loadedFromCache; roomRequest.newName.resize(channelNameLength); deserializer.extract((u8*)&roomRequest.newName[0], channelNameLength); if(callbackFuncs.changeRoomNameCallbackFunc) callbackFuncs.changeRoomNameCallbackFunc(roomRequest); room->name = std::move(roomRequest.newName); } break; } default: { odhtdb::Log::warn("Rooms::addNodeCallbackFunc: Got unexpected room data type %d", roomDataType); break; } } } catch(std::exception &e) { odhtdb::Log::warn("Failed to process add node request, reason: %s\n", e.what()); } } void Rooms::addUserCallbackFunc(const odhtdb::DatabaseAddUserRequest &request) { std::lock_guard lock(roomModifyMutex); auto roomIt = roomById.find(*request.nodeHash); if(roomIt == roomById.end()) { odhtdb::Log::error("User %s was added to node %s but the node has not been created", request.userToAddPublicKey->toString().c_str(), request.nodeHash->toString().c_str()); return; } auto room = roomIt->second; auto group = room->getGroupById(request.groupToAddUserTo); if(!group) { odhtdb::Log::error("User %s was added to node %s in a non-existing group %s", request.userToAddPublicKey->toString().c_str(), request.nodeHash->toString().c_str(), request.groupToAddUserTo.toString().c_str()); return; } auto addedByUser = room->getUserByPublicKey(*request.creatorPublicKey); if(!addedByUser) { odhtdb::Log::error("User was added to room by admin that doesn't exist in room.... (bug?)"); return; } auto user = room->addUserLocally(*request.userToAddPublicKey, group); bool isLocalUser = false; bool waitedToJoin = false; if(!room->localUser) { std::lock_guard waitingToJoinRoomLock(waitingToJoinRoomMutex); auto waitingToJoinRoomIt = waitingToJoinRoom.find(*request.nodeHash); if(waitingToJoinRoomIt != waitingToJoinRoom.end() && user->publicKey == waitingToJoinRoomIt->second->getPublicKey()) { room->setLocalUser(user, waitingToJoinRoomIt->second); waitingToJoinRoom.erase(waitingToJoinRoomIt); isLocalUser = true; waitedToJoin = true; } else { auto localUserIt = roomLocalUser.find(*request.nodeHash); if(localUserIt != roomLocalUser.end() && user->publicKey == localUserIt->second->getPublicKey()) { room->setLocalUser(user, localUserIt->second); isLocalUser = true; } } } if(callbackFuncs.addUserCallbackFunc) { RoomAddUserRequest callbackRequest; callbackRequest.room = room; callbackRequest.user = user; callbackRequest.addedByUser = addedByUser; callbackRequest.timestampSeconds = ntp::NtpTimestamp::fromCombined(request.timestamp).seconds; callbackRequest.loadedFromCache = request.loadedFromCache; callbackRequest.isLocalUser = isLocalUser; callbackRequest.waitedToJoin = waitedToJoin; callbackFuncs.addUserCallbackFunc(callbackRequest); } } void Rooms::connect(const char *address, u16 port, RoomCallbackFuncs callbackFuncs) { assert(callbackFuncs.connectCallbackFunc); std::thread([](const char *address, u16 port, RoomCallbackFuncs callbackFuncs) { try { callbackFuncs.connectCallbackFunc(std::shared_ptr(new Rooms(address, port, callbackFuncs)), nullptr); } catch(std::exception &e) { callbackFuncs.connectCallbackFunc(nullptr, e.what()); } }, address, port, callbackFuncs).detach(); } // TODO: Add logout void Rooms::loginUser(const std::string &username, const std::string &password) { std::lock_guard lock(roomModifyMutex); if(loggedIn) throw std::runtime_error(std::string("You are already logged in as ") + currentUsername); auto storedNodes = database->getStoredNodeUserInfoDecrypted(username, password); loggedIn = true; currentUsername = username; currentUserPassword = password; for(auto &nodeInfo : storedNodes) { roomLocalUser[nodeInfo.first] = nodeInfo.second.userKeyPair; roomEncryptionKey[nodeInfo.first] = nodeInfo.second.nodeEncryptionKey; auto roomId = std::make_shared(nodeInfo.first); odhtdb::DatabaseNode roomNode(nodeInfo.second.nodeEncryptionKey, roomId); database->seed(roomNode); database->loadNode(nodeInfo.first); } } void Rooms::registerUser(const std::string &username, const std::string &password) { std::lock_guard lock(roomModifyMutex); if(loggedIn) throw std::runtime_error(std::string("You are already logged in as ") + currentUsername); database->storeUserWithoutNodes(username, password); loggedIn = true; currentUsername = username; currentUserPassword = password; } std::shared_ptr Rooms::createRoom(const std::string &name) { std::lock_guard lock(roomModifyMutex); if(!loggedIn) throw std::runtime_error("You need to be logged in to create a room"); auto newNode = database->create(); roomLocalUser[*newNode->getRequestHash()] = newNode->getNodeAdminKeyPair(); roomEncryptionKey[*newNode->getRequestHash()] = newNode->getNodeEncryptionKey(); auto roomIt = roomById.find(*newNode->getRequestHash()); assert(roomIt != roomById.end()); if(roomIt != roomById.end()) { roomIt->second->encryptionKey = newNode->getNodeEncryptionKey(); auto user = roomIt->second->getUserByPublicKey(newNode->getNodeAdminKeyPair()->getPublicKey()); if(user) { roomIt->second->setLocalUser(user, newNode->getNodeAdminKeyPair()); } } odhtdb::DatabaseNode nodeInfo(newNode->getNodeEncryptionKey(), newNode->getRequestHash()); database->storeNodeInfoForUserEncrypted(nodeInfo, currentUsername, currentUserPassword, *newNode->getNodeAdminKeyPair()); roomIt->second->setName(name); return roomIt->second; } bool Rooms::requestJoinRoom(const std::string &inviteKey, const std::string &message) { if(!loggedIn) throw std::runtime_error("You need to be logged in to join a room"); if(inviteKey.size() != 130) { std::string errMsg = "Invalid invite key length. Expected to be 130 bytes, was "; errMsg += std::to_string(inviteKey.size()); errMsg += " byte(s)"; throw std::runtime_error(errMsg); } std::lock_guard waitingToJoinRoomLock(waitingToJoinRoomMutex); std::string roomIdHex = inviteKey.substr(0, 64); std::string roomIdBin = odhtdb::hex2bin(roomIdHex.c_str(), roomIdHex.size()); auto roomId = std::make_shared(); memcpy(roomId->getData(), roomIdBin.data(), roomIdBin.size()); if(waitingToJoinRoom.find(*roomId) != waitingToJoinRoom.end()) { fprintf(stderr, "You have already requested to join room %s\n", roomIdHex.c_str()); return false; } if(roomById.find(*roomId) != roomById.end()) { fprintf(stderr, "You are already a member of the room %s\n", roomIdHex.c_str()); return false; } std::string roomEncryptionKeyHex = inviteKey.substr(64, 64); std::string roomEncryptionKeyStr = odhtdb::hex2bin(roomEncryptionKeyHex.c_str(), roomEncryptionKeyHex.size()); auto newEncryptionKey = std::make_shared(new u8[odhtdb::ENCRYPTION_KEY_BYTE_SIZE], odhtdb::ENCRYPTION_KEY_BYTE_SIZE); memcpy(newEncryptionKey->data, roomEncryptionKeyStr.data(), roomEncryptionKeyStr.size()); std::string inviteKeyChecksum = inviteKey.substr(128, 2); if(inviteKeyChecksum != getInviteKeyChecksum(inviteKey.substr(0, 128))) throw std::runtime_error("Invalid invite key, checksum is incorrect. Did you type the invite key incorrectly?"); auto ourNewUser = std::make_shared(); sibs::SafeSerializer serializer; serializer.add(ourNewUser->getPublicKey().getData(), ourNewUser->getPublicKey().getSize()); serializer.add(message.data(), message.size()); odhtdb::DataView encryptionData { serializer.getBuffer().data(), serializer.getSize() }; odhtdb::DataView encryptionKey { (void*)newEncryptionKey->data, newEncryptionKey->size }; odhtdb::Encryption encryption(encryptionData, encryptionKey); sibs::SafeSerializer serializerFinish; serializerFinish.add((const u8*)encryption.getNonce().data, encryption.getNonce().size); serializerFinish.add((const u8*)encryption.getCipherText().data, encryption.getCipherText().size); odhtdb::InfoHash inviteInfoHash = odhtdb::InfoHash::generateHash((const u8*)inviteKey.data(), inviteKey.size()); roomLocalUser[*roomId] = ourNewUser; roomEncryptionKey[*roomId] = newEncryptionKey; waitingToJoinRoom[*roomId] = ourNewUser; database->sendCustomMessage(inviteInfoHash, serializerFinish.getBuffer().data(), serializerFinish.getSize()); odhtdb::DatabaseNode roomNode(newEncryptionKey, roomId); database->seed(roomNode); database->storeNodeInfoForUserEncrypted(roomNode, currentUsername, currentUserPassword, *ourNewUser); return true; } }