diff options
Diffstat (limited to 'src/remoteauth')
-rw-r--r-- | src/remoteauth/remoteauthclient.cpp | 351 | ||||
-rw-r--r-- | src/remoteauth/remoteauthclient.hpp | 95 | ||||
-rw-r--r-- | src/remoteauth/remoteauthdialog.cpp | 132 | ||||
-rw-r--r-- | src/remoteauth/remoteauthdialog.hpp | 38 | ||||
-rw-r--r-- | src/remoteauth/ssl.hpp | 31 |
5 files changed, 647 insertions, 0 deletions
diff --git a/src/remoteauth/remoteauthclient.cpp b/src/remoteauth/remoteauthclient.cpp new file mode 100644 index 0000000..7653b78 --- /dev/null +++ b/src/remoteauth/remoteauthclient.cpp @@ -0,0 +1,351 @@ +#ifdef WITH_QRLOGIN + +// clang-format off + +#include "remoteauthclient.hpp" +#include "http.hpp" +#include <nlohmann/json.hpp> +#include <spdlog/fmt/bin_to_hex.h> + +// clang-format on + +RemoteAuthClient::RemoteAuthClient() + : m_ws("remote-auth-ws") + , m_log(spdlog::get("remote-auth")) { + m_ws.signal_open().connect(sigc::mem_fun(*this, &RemoteAuthClient::OnWebsocketOpen)); + m_ws.signal_close().connect(sigc::mem_fun(*this, &RemoteAuthClient::OnWebsocketClose)); + m_ws.signal_message().connect(sigc::mem_fun(*this, &RemoteAuthClient::OnWebsocketMessage)); + + m_dispatcher.connect(sigc::mem_fun(*this, &RemoteAuthClient::OnDispatch)); +} + +RemoteAuthClient::~RemoteAuthClient() { + Stop(); +} + +void RemoteAuthClient::Start() { + if (IsConnected()) { + Stop(); + } + + m_connected = true; + m_heartbeat_waiter.revive(); + m_ws.StartConnection("wss://remote-auth-gateway.discord.gg/?v=2"); +} + +void RemoteAuthClient::Stop() { + if (!IsConnected()) { + m_log->warn("Requested stop while not connected"); + return; + } + + m_connected = false; + if (m_timeout_conn) m_timeout_conn.disconnect(); + m_ws.Stop(1000); + m_heartbeat_waiter.kill(); + if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); +} + +bool RemoteAuthClient::IsConnected() const noexcept { + return m_connected; +} + +void RemoteAuthClient::OnGatewayMessage(const std::string &str) { + m_log->trace(str); + auto j = nlohmann::json::parse(str); + const auto opcode = j.at("op").get<std::string>(); + if (opcode == "hello") { + HandleGatewayHello(j); + } else if (opcode == "nonce_proof") { + HandleGatewayNonceProof(j); + } else if (opcode == "pending_remote_init") { + HandleGatewayPendingRemoteInit(j); + } else if (opcode == "pending_ticket") { + HandleGatewayPendingTicket(j); + } else if (opcode == "pending_login") { + HandleGatewayPendingLogin(j); + } else if (opcode == "cancel") { + HandleGatewayCancel(j); + } +} + +void RemoteAuthClient::HandleGatewayHello(const nlohmann::json &j) { + const auto timeout_ms = j.at("timeout_ms").get<int>(); + const auto heartbeat_interval = j.at("heartbeat_interval").get<int>(); + m_log->debug("Timeout: {}, Heartbeat: {}", timeout_ms, heartbeat_interval); + + m_heartbeat_msec = heartbeat_interval; + m_heartbeat_thread = std::thread(&RemoteAuthClient::HeartbeatThread, this); + + m_timeout_conn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &RemoteAuthClient::OnTimeout), timeout_ms); + + Init(); + + m_signal_hello.emit(); +} + +void RemoteAuthClient::HandleGatewayNonceProof(const nlohmann::json &j) { + m_log->debug("Received encrypted nonce"); + + const auto encrypted_nonce = Glib::Base64::decode(j.at("encrypted_nonce").get<std::string>()); + const auto proof = Decrypt(reinterpret_cast<const unsigned char *>(encrypted_nonce.data()), encrypted_nonce.size()); + auto proof_encoded = Glib::Base64::encode(std::string(proof.begin(), proof.end())); + + std::replace(proof_encoded.begin(), proof_encoded.end(), '/', '_'); + std::replace(proof_encoded.begin(), proof_encoded.end(), '+', '-'); + proof_encoded.erase(std::remove(proof_encoded.begin(), proof_encoded.end(), '='), proof_encoded.end()); + + nlohmann::json reply; + reply["op"] = "nonce_proof"; + reply["nonce"] = proof_encoded; + m_ws.Send(reply); +} + +void RemoteAuthClient::HandleGatewayPendingRemoteInit(const nlohmann::json &j) { + m_log->debug("Received fingerprint"); + + m_signal_fingerprint.emit(j.at("fingerprint").get<std::string>()); +} + +void RemoteAuthClient::HandleGatewayPendingTicket(const nlohmann::json &j) { + const auto encrypted_payload = Glib::Base64::decode(j.at("encrypted_user_payload").get<std::string>()); + const auto payload = Decrypt(reinterpret_cast<const unsigned char *>(encrypted_payload.data()), encrypted_payload.size()); + + m_log->trace("User payload: {}", std::string(payload.begin(), payload.end())); + + const std::vector<Glib::ustring> user_info = Glib::Regex::split_simple(":", std::string(payload.begin(), payload.end())); + Snowflake user_id; + std::string discriminator; + std::string avatar_hash; + std::string username; + if (user_info.size() >= 4) { + user_id = Snowflake(user_info[0]); + discriminator = user_info[1]; + avatar_hash = user_info[2]; + username = user_info[3]; + } + + m_signal_pending_ticket.emit(user_id, discriminator, avatar_hash, username); +} + +void RemoteAuthClient::HandleGatewayPendingLogin(const nlohmann::json &j) { + Abaddon::Get().GetDiscordClient().RemoteAuthLogin(j.at("ticket").get<std::string>(), sigc::mem_fun(*this, &RemoteAuthClient::OnRemoteAuthLoginResponse)); + m_signal_pending_login.emit(); +} + +void RemoteAuthClient::HandleGatewayCancel(const nlohmann::json &j) { + Stop(); + Start(); +} + +void RemoteAuthClient::OnRemoteAuthLoginResponse(const std::optional<std::string> &encrypted_token, DiscordError err) { + if (!encrypted_token.has_value()) { + m_log->error("Remote auth login failed: {}", static_cast<int>(err)); + if (err == DiscordError::CAPTCHA_REQUIRED) { + m_signal_error.emit("Discord is requiring a captcha. You must use a web browser to log in."); + } else { + m_signal_error.emit("An error occurred. Try again."); + } + return; + } + + const auto encrypted = Glib::Base64::decode(*encrypted_token); + const auto token = Decrypt(reinterpret_cast<const unsigned char *>(encrypted.data()), encrypted.size()); + m_signal_token.emit(std::string(token.begin(), token.end())); +} + +void RemoteAuthClient::Init() { + GenerateKey(); + const auto key = GetEncodedPublicKey(); + if (key.empty()) { + m_log->error("Something went wrong"); + // todo disconnect + return; + } + + nlohmann::json msg; + msg["op"] = "init"; + msg["encoded_public_key"] = key; + m_ws.Send(msg); +} + +void RemoteAuthClient::GenerateKey() { + // you javascript people have it so easy + // check out this documentation https://www.openssl.org/docs/man1.1.1/man3/PEM_write_bio_PUBKEY.html + + m_pkey_ctx.reset(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)); + if (!m_pkey_ctx) { + m_log->error("Failed to create RSA context"); + return; + } + + if (EVP_PKEY_keygen_init(m_pkey_ctx.get()) <= 0) { + m_log->error("Failed to initialize RSA context"); + return; + } + + if (EVP_PKEY_CTX_set_rsa_keygen_bits(m_pkey_ctx.get(), 2048) <= 0) { + m_log->error("Failed to set keygen bits"); + return; + } + + EVP_PKEY *pkey_tmp = nullptr; + if (EVP_PKEY_keygen(m_pkey_ctx.get(), &pkey_tmp) <= 0) { + m_log->error("Failed to generate keypair"); + return; + } + m_pkey.reset(pkey_tmp); + + m_dec_ctx.reset(EVP_PKEY_CTX_new(m_pkey.get(), nullptr)); + if (EVP_PKEY_decrypt_init(m_dec_ctx.get()) <= 0) { + m_log->error("Failed to initialize RSA decrypt context"); + return; + } + + if (EVP_PKEY_CTX_set_rsa_padding(m_dec_ctx.get(), RSA_PKCS1_OAEP_PADDING) <= 0) { + m_log->error("EVP_PKEY_CTX_set_rsa_padding failed"); + return; + } + + if (EVP_PKEY_CTX_set_rsa_oaep_md(m_dec_ctx.get(), EVP_sha256()) <= 0) { + m_log->error("EVP_PKEY_CTX_set_rsa_oaep_md failed"); + return; + } + + if (EVP_PKEY_CTX_set_rsa_mgf1_md(m_dec_ctx.get(), EVP_sha256()) <= 0) { + m_log->error("EVP_PKEY_CTX_set_rsa_mgf1_md"); + return; + } +} + +std::string RemoteAuthClient::GetEncodedPublicKey() const { + auto bio = BIO_ptr(BIO_new(BIO_s_mem()), BIO_free); + if (!bio) { + m_log->error("Failed to create BIO"); + return {}; + } + + if (PEM_write_bio_PUBKEY(bio.get(), m_pkey.get()) <= 0) { + m_log->error("Failed to write public key to BIO"); + return {}; + } + + // i think this is freed when the bio is too + BUF_MEM *mem = nullptr; + if (BIO_get_mem_ptr(bio.get(), &mem) <= 0) { + m_log->error("Failed to get BIO mem buf"); + return {}; + } + + if (mem->data == nullptr || mem->length == 0) { + m_log->error("BIO mem buf is null or of zero length"); + return {}; + } + + std::string pem_pubkey(mem->data, mem->length); + // isolate key + pem_pubkey.erase(0, pem_pubkey.find("\n") + 1); + pem_pubkey.erase(pem_pubkey.rfind("\n-")); + size_t pos; + while ((pos = pem_pubkey.find("\n")) != std::string::npos) { + pem_pubkey.erase(pos, 1); + } + return pem_pubkey; +} + +std::vector<uint8_t> RemoteAuthClient::Decrypt(const unsigned char *in, size_t inlen) const { + // get length + size_t outlen; + if (EVP_PKEY_decrypt(m_dec_ctx.get(), nullptr, &outlen, in, inlen) <= 0) { + m_log->error("Failed to get length when decrypting"); + return {}; + } + + std::vector<uint8_t> ret(outlen); + if (EVP_PKEY_decrypt(m_dec_ctx.get(), ret.data(), &outlen, in, inlen) <= 0) { + m_log->error("Failed to decrypt"); + return {}; + } + ret.resize(outlen); + return ret; +} + +void RemoteAuthClient::OnWebsocketOpen() { + m_log->info("Websocket opened"); +} + +void RemoteAuthClient::OnWebsocketClose(const ix::WebSocketCloseInfo &info) { + if (info.remote) { + m_log->debug("Websocket closed (remote): {} ({})", info.code, info.reason); + if (m_connected) { + m_signal_error.emit("Error. Websocket closed (remote): " + std::to_string(info.code) + " (" + info.reason + ")"); + } + } else { + m_log->debug("Websocket closed (local): {} ({})", info.code, info.reason); + if (m_connected) { + m_signal_error.emit("Error. Websocket closed (local): " + std::to_string(info.code) + " (" + info.reason + ")"); + } + } +} + +void RemoteAuthClient::OnWebsocketMessage(const std::string &data) { + m_dispatch_mutex.lock(); + m_dispatch_queue.push(data); + m_dispatcher.emit(); + m_dispatch_mutex.unlock(); +} + +void RemoteAuthClient::HeartbeatThread() { + while (true) { + if (!m_heartbeat_waiter.wait_for(std::chrono::milliseconds(m_heartbeat_msec))) break; + + nlohmann::json hb; + hb["op"] = "heartbeat"; + m_ws.Send(hb); + } +} + +void RemoteAuthClient::OnDispatch() { + m_dispatch_mutex.lock(); + if (m_dispatch_queue.empty()) { + m_dispatch_mutex.unlock(); + return; + } + auto msg = std::move(m_dispatch_queue.front()); + m_dispatch_queue.pop(); + m_dispatch_mutex.unlock(); + OnGatewayMessage(msg); +} + +bool RemoteAuthClient::OnTimeout() { + m_log->trace("Socket timeout"); + Stop(); + Start(); + return false; // disconnect +} + +RemoteAuthClient::type_signal_hello RemoteAuthClient::signal_hello() { + return m_signal_hello; +} + +RemoteAuthClient::type_signal_fingerprint RemoteAuthClient::signal_fingerprint() { + return m_signal_fingerprint; +} + +RemoteAuthClient::type_signal_pending_ticket RemoteAuthClient::signal_pending_ticket() { + return m_signal_pending_ticket; +} + +RemoteAuthClient::type_signal_pending_login RemoteAuthClient::signal_pending_login() { + return m_signal_pending_login; +} + +RemoteAuthClient::type_signal_token RemoteAuthClient::signal_token() { + return m_signal_token; +} + +RemoteAuthClient::type_signal_error RemoteAuthClient::signal_error() { + return m_signal_error; +} + +#endif diff --git a/src/remoteauth/remoteauthclient.hpp b/src/remoteauth/remoteauthclient.hpp new file mode 100644 index 0000000..6ab6dbb --- /dev/null +++ b/src/remoteauth/remoteauthclient.hpp @@ -0,0 +1,95 @@ +#pragma once + +#ifdef WITH_QRLOGIN + +// clang-format off + +#include <string> +#include <queue> +#include <spdlog/logger.h> +#include "ssl.hpp" +#include "discord/waiter.hpp" +#include "discord/websocket.hpp" + +// clang-format on + +class RemoteAuthClient { +public: + RemoteAuthClient(); + ~RemoteAuthClient(); + + void Start(); + void Stop(); + + [[nodiscard]] bool IsConnected() const noexcept; + +private: + void OnGatewayMessage(const std::string &str); + void HandleGatewayHello(const nlohmann::json &j); + void HandleGatewayNonceProof(const nlohmann::json &j); + void HandleGatewayPendingRemoteInit(const nlohmann::json &j); + void HandleGatewayPendingTicket(const nlohmann::json &j); + void HandleGatewayPendingLogin(const nlohmann::json &j); + void HandleGatewayCancel(const nlohmann::json &j); + + void OnRemoteAuthLoginResponse(const std::optional<std::string> &encrypted_token, DiscordError err); + + void Init(); + + void GenerateKey(); + std::string GetEncodedPublicKey() const; + + std::vector<uint8_t> Decrypt(const unsigned char *in, size_t inlen) const; + + void OnWebsocketOpen(); + void OnWebsocketClose(const ix::WebSocketCloseInfo &info); + void OnWebsocketMessage(const std::string &str); + + void HeartbeatThread(); + + int m_heartbeat_msec; + Waiter m_heartbeat_waiter; + std::thread m_heartbeat_thread; + + Glib::Dispatcher m_dispatcher; + std::queue<std::string> m_dispatch_queue; + std::mutex m_dispatch_mutex; + + void OnDispatch(); + + bool OnTimeout(); + sigc::connection m_timeout_conn; + + Websocket m_ws; + bool m_connected = false; + + std::shared_ptr<spdlog::logger> m_log; + + EVP_PKEY_CTX_ptr m_pkey_ctx; + EVP_PKEY_CTX_ptr m_dec_ctx; + EVP_PKEY_ptr m_pkey; + +public: + using type_signal_hello = sigc::signal<void()>; + using type_signal_fingerprint = sigc::signal<void(std::string)>; + using type_signal_pending_ticket = sigc::signal<void(Snowflake, std::string, std::string, std::string)>; + using type_signal_pending_login = sigc::signal<void()>; + using type_signal_token = sigc::signal<void(std::string)>; + using type_signal_error = sigc::signal<void(std::string)>; + type_signal_hello signal_hello(); + type_signal_fingerprint signal_fingerprint(); + type_signal_pending_ticket signal_pending_ticket(); + type_signal_pending_login signal_pending_login(); + type_signal_token signal_token(); + type_signal_error signal_error(); + +private: + type_signal_hello m_signal_hello; + type_signal_fingerprint m_signal_fingerprint; + type_signal_pending_ticket m_signal_pending_ticket; + type_signal_pending_login m_signal_pending_login; + type_signal_token m_signal_token; + type_signal_error m_signal_error; +}; + +#endif diff --git a/src/remoteauth/remoteauthdialog.cpp b/src/remoteauth/remoteauthdialog.cpp new file mode 100644 index 0000000..7975b4e --- /dev/null +++ b/src/remoteauth/remoteauthdialog.cpp @@ -0,0 +1,132 @@ +#ifdef WITH_QRLOGIN + +// clang-format off + +#include "remoteauthdialog.hpp" +#include <qrcodegen.hpp> + +// clang-format on + +RemoteAuthDialog::RemoteAuthDialog(Gtk::Window &parent) + : Gtk::Dialog("Login with QR Code", parent, true) + , m_layout(Gtk::ORIENTATION_VERTICAL) + , m_ok("OK") + , m_cancel("Cancel") + , m_bbox(Gtk::ORIENTATION_HORIZONTAL) { + set_default_size(300, 50); + get_style_context()->add_class("app-window"); + get_style_context()->add_class("app-popup"); + + m_ok.signal_clicked().connect([&]() { + response(Gtk::RESPONSE_OK); + }); + + m_cancel.signal_clicked().connect([&]() { + response(Gtk::RESPONSE_CANCEL); + }); + + m_bbox.pack_start(m_ok, Gtk::PACK_SHRINK); + m_bbox.pack_start(m_cancel, Gtk::PACK_SHRINK); + m_bbox.set_layout(Gtk::BUTTONBOX_END); + + m_ra.signal_hello().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnHello)); + m_ra.signal_fingerprint().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnFingerprint)); + m_ra.signal_pending_ticket().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnPendingTicket)); + m_ra.signal_pending_login().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnPendingLogin)); + m_ra.signal_token().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnToken)); + m_ra.signal_error().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnError)); + + m_ra.Start(); + + m_image.set_size_request(256, 256); + + m_status.set_text("Connecting..."); + m_status.set_hexpand(true); + m_status.set_halign(Gtk::ALIGN_CENTER); + + m_layout.add(m_image); + m_layout.add(m_status); + m_layout.add(m_bbox); + get_content_area()->add(m_layout); + + show_all_children(); +} + +std::string RemoteAuthDialog::GetToken() { + return m_token; +} + +void RemoteAuthDialog::OnHello() { + m_status.set_text("Handshaking..."); +} + +void RemoteAuthDialog::OnFingerprint(const std::string &fingerprint) { + m_status.set_text("Waiting for mobile device..."); + + const auto url = "https://discord.com/ra/" + fingerprint; + + const auto level = qrcodegen::QrCode::Ecc::QUARTILE; + const auto qr = qrcodegen::QrCode::encodeText(url.c_str(), level); + + int size = qr.getSize(); + const int border = 4; + + const auto module_set = "192 0 255"; + const auto module_clr = "255 255 255"; + + std::ostringstream sb; + sb << "P3\n"; + sb << size + border * 2 << " " << size + border * 2 << " 255\n"; + for (int y = -border; y < size + border; y++) { + for (int x = -border; x < size + border; x++) { + if (qr.getModule(x, y)) { + sb << module_set << "\n"; + } else { + sb << module_clr << "\n"; + } + } + } + + const auto img = sb.str(); + + auto loader = Gdk::PixbufLoader::create(); + loader->write(reinterpret_cast<const guint8 *>(img.data()), img.size()); + loader->close(); + const auto pb = loader->get_pixbuf()->scale_simple(256, 256, Gdk::INTERP_NEAREST); + + m_image.property_pixbuf() = pb; +} + +void RemoteAuthDialog::OnPendingTicket(Snowflake user_id, const std::string &discriminator, const std::string &avatar_hash, const std::string &username) { + Glib::ustring name = username; + if (discriminator != "0") { + name += "#" + discriminator; + } + m_status.set_text("Waiting for confirmation... (" + name + ")"); + + if (!avatar_hash.empty()) { + const auto url = "https://cdn.discordapp.com/avatars/" + std::to_string(user_id) + "/" + avatar_hash + ".png?size=256"; + const auto cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) { + m_image.property_pixbuf() = pb->scale_simple(256, 256, Gdk::INTERP_BILINEAR); + }; + Abaddon::Get().GetImageManager().LoadFromURL(url, sigc::track_obj(cb, *this)); + } +} + +void RemoteAuthDialog::OnPendingLogin() { + m_status.set_text("Logging in!"); +} + +void RemoteAuthDialog::OnToken(const std::string &token) { + m_token = token; + m_ra.Stop(); + response(Gtk::RESPONSE_OK); +} + +void RemoteAuthDialog::OnError(const std::string &error) { + m_ra.Stop(); + Abaddon::Get().ShowConfirm(error, dynamic_cast<Gtk::Window *>(get_toplevel())); + response(Gtk::RESPONSE_CANCEL); +} + +#endif diff --git a/src/remoteauth/remoteauthdialog.hpp b/src/remoteauth/remoteauthdialog.hpp new file mode 100644 index 0000000..465a188 --- /dev/null +++ b/src/remoteauth/remoteauthdialog.hpp @@ -0,0 +1,38 @@ +#pragma once + +#ifdef WITH_QRLOGIN + +// clang-format off + +#include <gtkmm/dialog.h> +#include "remoteauthclient.hpp" + +// clang-format on + +class RemoteAuthDialog : public Gtk::Dialog { +public: + RemoteAuthDialog(Gtk::Window &parent); + std::string GetToken(); + +protected: + Gtk::Image m_image; + Gtk::Label m_status; + Gtk::Box m_layout; + Gtk::Button m_ok; + Gtk::Button m_cancel; + Gtk::ButtonBox m_bbox; + +private: + RemoteAuthClient m_ra; + + void OnHello(); + void OnFingerprint(const std::string &fingerprint); + void OnPendingTicket(Snowflake user_id, const std::string &discriminator, const std::string &avatar_hash, const std::string &username); + void OnPendingLogin(); + void OnToken(const std::string &token); + void OnError(const std::string &error); + + std::string m_token; +}; + +#endif diff --git a/src/remoteauth/ssl.hpp b/src/remoteauth/ssl.hpp new file mode 100644 index 0000000..1753bd3 --- /dev/null +++ b/src/remoteauth/ssl.hpp @@ -0,0 +1,31 @@ +#pragma once +#include <memory> +#include <openssl/bio.h> +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/rsa.h> +#include <openssl/sha.h> + +struct EVP_PKEY_CTX_deleter { + void operator()(EVP_PKEY_CTX *ptr) const { + EVP_PKEY_CTX_free(ptr); + } +}; + +struct EVP_PKEY_deleter { + void operator()(EVP_PKEY *ptr) const { + EVP_PKEY_free(ptr); + } +}; + +struct EVP_MD_CTX_deleter { + void operator()(EVP_MD_CTX *ptr) const { + EVP_MD_CTX_free(ptr); + } +}; + +using EVP_PKEY_CTX_ptr = std::unique_ptr<EVP_PKEY_CTX, EVP_PKEY_CTX_deleter>; +using EVP_PKEY_ptr = std::unique_ptr<EVP_PKEY, EVP_PKEY_deleter>; +using EVP_MD_CTX_ptr = std::unique_ptr<EVP_MD_CTX, EVP_MD_CTX_deleter>; +using BIO_ptr = std::unique_ptr<BIO, decltype(&BIO_free)>; +using BUF_MEM_ptr = std::unique_ptr<BUF_MEM, decltype(&BUF_MEM_free)>; |