diff --git a/src/signal_negotiation.cpp b/src/signal_negotiation.cpp index d4b8180..59dca75 100644 --- a/src/signal_negotiation.cpp +++ b/src/signal_negotiation.cpp @@ -70,11 +70,16 @@ bool SignalNegotiation::login_user(websocketpp::connection_hdl hdl, // Check if AddDevice failed if (ret_host_id.empty()) { + std::string reason = "Failed to register device"; + if (!host_id.empty() && + device_db_manager_->VerifyDevice(host_id, password) == -1) { + reason = "Incorrect password"; + } LOG_ERROR("Failed to add device for host_id [{}]", host_id); json message = {{"type", "login"}, {"user_id", ""}, {"status", "fail"}, - {"reason", "Failed to register device"}}; + {"reason", reason}}; send_msg_(hdl, message); return true; } @@ -426,6 +431,49 @@ bool SignalNegotiation::new_candidate_mid(websocketpp::connection_hdl hdl, return true; } +bool SignalNegotiation::change_password(websocketpp::connection_hdl hdl, + const json& j) { + json message = {{"type", "change_password"}}; + std::string request_id; + if (!GetStringField(j, "request_id", request_id) || request_id.empty()) { + message["status"] = "fail"; + message["reason"] = "Missing or invalid request ID"; + send_msg_(hdl, message); + return true; + } + message["request_id"] = request_id; + + const std::string user_id = transmission_manager_->GetUserId(hdl); + message["user_id"] = user_id; + + if (user_id.empty()) { + message["status"] = "fail"; + message["reason"] = "Not authenticated"; + } else if (user_id.rfind("C-", 0) == 0 || + user_id.rfind("web-", 0) == 0) { + // Controller and browser identities are temporary and are not authenticated + // against a persisted device password. + message["status"] = "fail"; + message["reason"] = "Password changes are not allowed for this identity"; + } else { + std::string new_password; + if (!GetStringField(j, "new_password", new_password) || + new_password.size() != 6) { + message["status"] = "fail"; + message["reason"] = "Password must contain exactly 6 characters"; + } else if (!device_db_manager_->UpdatePassword(user_id, new_password)) { + message["status"] = "fail"; + message["reason"] = "Failed to update password"; + } else { + message["status"] = "success"; + LOG_INFO("Authenticated client [{}] changed its device password", user_id); + } + } + + send_msg_(hdl, message); + return true; +} + bool SignalNegotiation::turn_credentials(websocketpp::connection_hdl hdl, const json& j) { (void)j; diff --git a/src/signal_negotiation.h b/src/signal_negotiation.h index 1b520b8..b2ef4f3 100644 --- a/src/signal_negotiation.h +++ b/src/signal_negotiation.h @@ -36,6 +36,7 @@ class SignalNegotiation { bool answer(websocketpp::connection_hdl hdl, const json& j); bool new_candidate(websocketpp::connection_hdl hdl, const json& j); bool new_candidate_mid(websocketpp::connection_hdl hdl, const json& j); + bool change_password(websocketpp::connection_hdl hdl, const json& j); bool turn_credentials(websocketpp::connection_hdl hdl, const json& j); void OnWebClientDisconnect(const std::string& user_id); diff --git a/src/signal_server.cpp b/src/signal_server.cpp index c938fa4..4763087 100644 --- a/src/signal_server.cpp +++ b/src/signal_server.cpp @@ -828,6 +828,9 @@ void SignalServer::OnMessage(websocketpp::connection_hdl hdl, case "new_candidate_mid"_H: signal_negotiation_->new_candidate_mid(hdl, j); break; + case "change_password"_H: + signal_negotiation_->change_password(hdl, j); + break; case "turn_credentials"_H: signal_negotiation_->turn_credentials(hdl, j); break; diff --git a/tests/signal_negotiation_test.cpp b/tests/signal_negotiation_test.cpp index 781ffc1..ec5d4ba 100644 --- a/tests/signal_negotiation_test.cpp +++ b/tests/signal_negotiation_test.cpp @@ -84,12 +84,52 @@ int main() { "wrong device password login response has login type"); expect(sent_messages[0].value("status", "") == "fail", "wrong device password login fails"); + expect(sent_messages[0].value("reason", "") == "Incorrect password", + "wrong device password login reports a specific reason"); } expect(db.VerifyDevice(offline_host.device_id, offline_host.password) == 0, "wrong device password login does not replace original password"); expect(db.VerifyDevice(offline_host.device_id, "attacker-password") != 0, "wrong device password is not accepted after failed login"); + sent_messages.clear(); + negotiation.change_password( + attacker_hdl, + {{"type", "change_password"}, {"new_password", "654321"}}); + expect(sent_messages.size() == 1 && + sent_messages[0].value("status", "") == "fail" && + sent_messages[0].value("reason", "") == + "Missing or invalid request ID", + "password change requires a request id"); + + sent_messages.clear(); + negotiation.change_password( + attacker_hdl, + {{"type", "change_password"}, + {"request_id", "unauthenticated"}, + {"new_password", "654321"}}); + expect(sent_messages.size() == 1 && + sent_messages[0].value("status", "") == "fail" && + sent_messages[0].value("reason", "") == "Not authenticated", + "unauthenticated connection cannot change a device password"); + expect(db.VerifyDevice(offline_host.device_id, offline_host.password) == 0, + "unauthenticated password change leaves the password unchanged"); + + sent_messages.clear(); + auto controller_connection = std::make_shared(5); + websocketpp::connection_hdl controller_hdl(controller_connection); + transmission->BindUserToWsHandle("C-controller", controller_hdl); + negotiation.change_password( + controller_hdl, + {{"type", "change_password"}, + {"request_id", "controller"}, + {"new_password", "654321"}}); + expect(sent_messages.size() == 1 && + sent_messages[0].value("status", "") == "fail", + "temporary controller identity cannot change a device password"); + expect(db.VerifyDevice(offline_host.device_id, offline_host.password) == 0, + "controller password change leaves the device password unchanged"); + sent_messages.clear(); auto authenticated_connection = std::make_shared(3); websocketpp::connection_hdl authenticated_hdl(authenticated_connection); @@ -132,6 +172,47 @@ int main() { sent_messages[0].contains("turn"), "authenticated client can explicitly refresh TURN credentials"); + sent_messages.clear(); + negotiation.change_password( + authenticated_hdl, + {{"type", "change_password"}, + {"request_id", "malformed"}, + {"new_password", "short"}}); + expect(sent_messages.size() == 1 && + sent_messages[0].value("status", "") == "fail", + "authenticated client cannot set a malformed password"); + expect(db.VerifyDevice(offline_host.device_id, offline_host.password) == 0, + "malformed password change leaves the password unchanged"); + + sent_messages.clear(); + const std::string new_password = "654321"; + negotiation.change_password( + authenticated_hdl, + {{"type", "change_password"}, + {"request_id", "successful"}, + {"new_password", new_password}}); + expect(sent_messages.size() == 1 && + sent_messages[0].value("type", "") == "change_password" && + sent_messages[0].value("status", "") == "success" && + sent_messages[0].value("request_id", "") == "successful" && + sent_messages[0].value("user_id", "") == offline_host.device_id, + "authenticated device can change its own password"); + expect(db.VerifyDevice(offline_host.device_id, offline_host.password) != 0, + "old password is rejected after a successful password change"); + expect(db.VerifyDevice(offline_host.device_id, new_password) == 0, + "new password is accepted after a successful password change"); + + sent_messages.clear(); + auto relogin_connection = std::make_shared(4); + websocketpp::connection_hdl relogin_hdl(relogin_connection); + negotiation.login_user( + relogin_hdl, + {{"type", "login"}, + {"user_id", offline_host.device_id + "@" + new_password}}); + expect(sent_messages.size() == 1 && + sent_messages[0].value("status", "") == "success", + "device can log in again with its changed password"); + } std::filesystem::remove(db_path);