aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt4
-rw-r--r--src/aoapplication.h3
-rw-r--r--src/auth_flow.cpp159
-rw-r--r--src/auth_flow.h66
-rw-r--r--src/courtroom.cpp13
-rw-r--r--src/courtroom.h1
-rw-r--r--src/main.cpp1
-rw-r--r--src/packet_distribution.cpp2
-rw-r--r--src/saved_auth.cpp124
-rw-r--r--src/saved_auth.h17
-rw-r--r--src/serverdata.h3
11 files changed, 393 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 167d827..c5930f7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -121,6 +121,10 @@ qt_add_executable(Attorney_Online
src/ext_packet.cpp
src/vli.h
src/vli.c
+ src/auth_flow.h
+ src/auth_flow.cpp
+ src/saved_auth.h
+ src/saved_auth.cpp
)
if(CMAKE_BUILD_TYPE STREQUAL "Dev")
diff --git a/src/aoapplication.h b/src/aoapplication.h
index 548d4d0..d8d0c00 100644
--- a/src/aoapplication.h
+++ b/src/aoapplication.h
@@ -6,6 +6,7 @@
#include "discord_rich_presence.h"
#include "ext_packet.h"
#include "keyring.h"
+#include "saved_auth.h"
#include "serverdata.h"
#include "widgets/aooptionsdialog.h"
@@ -345,6 +346,8 @@ public:
ma_decoder_config mus_decoder_config;
KeyringModel keyring_model;
+ SavedAuth saved_auth;
+ QString ex_auth_username;
private:
QVector<ServerInfo> server_list;
diff --git a/src/auth_flow.cpp b/src/auth_flow.cpp
new file mode 100644
index 0000000..07544e8
--- /dev/null
+++ b/src/auth_flow.cpp
@@ -0,0 +1,159 @@
+#include <QHeaderView>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QTableView>
+#include <QVBoxLayout>
+
+#include "auth_flow.h"
+#include "keyring.h"
+
+#include "file_functions.h"
+
+// This function is supposed to open the authentication dialog with various
+// fields like method selection and fields to enter password or select a key,
+// but for now, it'll simply submit a public key auth request. Hostname
+// parameter is unused.
+void start_auth_flow(AOApplication *ao_app, QString username)
+{
+ ao_app->ex_auth_username = username;
+ AuthRequest req;
+ req.username = username;
+ req.method = AuthMethod::certificate;
+ ao_app->send_ex_message(serializeAuthRequest(req));
+}
+
+KeySelectDialog::KeySelectDialog(KeyringModel *model, QStringView username, QWidget *parent)
+ : QDialog(parent)
+ , m_model(model)
+{
+ this->setAttribute(Qt::WA_DeleteOnClose);
+ this->setWindowTitle(QString("Select key for %1").arg(username));
+ auto view = new QTableView(this);
+ view->setModel(m_model);
+ view->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
+ view->setSelectionBehavior(QAbstractItemView::SelectRows);
+ view->setSelectionMode(QAbstractItemView::SingleSelection);
+ QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
+ QVBoxLayout *layout = new QVBoxLayout(this);
+ layout->addWidget(view);
+ layout->addWidget(buttons);
+ connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ connect(buttons, &QDialogButtonBox::accepted, this, [this, view] {
+ auto rows = view->selectionModel()->selectedRows();
+ if (rows.isEmpty())
+ {
+ return;
+ }
+
+ QByteArray key_id = m_model->data(rows.first(), KeyringModel::KeyIDRole).toByteArray();
+ QString key_name = m_model->data(rows.first()).toString();
+
+ emit key_selected(key_id, key_name);
+ });
+ connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, [view, buttons](const QItemSelection &, const QItemSelection &) {
+ bool selected = !view->selectionModel()->selectedRows().isEmpty();
+ buttons->button(QDialogButtonBox::Ok)->setEnabled(selected);
+ });
+}
+
+KeyPassphraseDialog::KeyPassphraseDialog(QStringView key_name, QWidget *parent)
+ : QDialog(parent)
+{
+ this->setWindowTitle(QString("Enter passphrase for key %1").arg(key_name));
+ QVBoxLayout *pw_layout = new QVBoxLayout(this);
+ pw_layout->addWidget(new QLabel(QStringLiteral("Passphrase:"), this));
+ m_pw_line = new QLineEdit(this);
+ m_pw_line->setEchoMode(QLineEdit::Password);
+ pw_layout->addWidget(m_pw_line);
+ QDialogButtonBox *pw_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ pw_layout->addWidget(pw_buttons);
+ m_err_lbl = new QLabel(this);
+ m_err_lbl->setVisible(false);
+ pw_layout->addWidget(m_err_lbl);
+ connect(pw_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ connect(pw_buttons, &QDialogButtonBox::accepted, this, [this] {
+ emit passphrase_submitted(m_pw_line->text().toUtf8());
+ m_pw_line->clear();
+ // zero the buffer
+ });
+}
+
+void KeyPassphraseDialog::display_error(ResponseResult error) const
+{
+ QString error_text;
+ switch (error)
+ {
+ case ResponseResult::unsupported_version:
+ error_text = QStringLiteral("Unsupported key version.");
+ break;
+ case ResponseResult::decryption_failed:
+ error_text = QStringLiteral("Wrong passphrase (or corrupted key).");
+ break;
+ default:
+ error_text = QString("Error unlocking key (code %1). Catastrophic failure.").arg((int)error);
+ break;
+ }
+ m_err_lbl->setText(error_text);
+ m_err_lbl->setVisible(true);
+}
+
+AuthFlow::AuthFlow(AOApplication *ao_app, const AuthChallenge &challenge, QWidget *parent)
+ : QObject(parent)
+ , m_ao_app(ao_app)
+ , m_challenge(challenge)
+{
+ auto saved_key = ao_app->saved_auth.lookup(ao_app->m_serverdata.m_server_hostname.toUtf8(), ao_app->ex_auth_username.toUtf8());
+ if (!saved_key.isEmpty())
+ {
+ m_mode = FlowMode::Saved;
+ m_key_dlg = nullptr;
+ on_key_selected(saved_key, QString("%1@%2 (saved)").arg(ao_app->ex_auth_username, ao_app->m_serverdata.m_server_hostname));
+ }
+ else
+ {
+ m_mode = FlowMode::Default;
+ m_key_dlg = new KeySelectDialog(&ao_app->keyring_model, ao_app->ex_auth_username, parent);
+ connect(m_key_dlg, &KeySelectDialog::key_selected, this, &AuthFlow::on_key_selected);
+ m_key_dlg->open();
+ }
+}
+
+void AuthFlow::on_key_selected(QByteArrayView key_id, QStringView key_name)
+{
+ m_pwd_dlg = new KeyPassphraseDialog(key_name, m_key_dlg);
+ connect(m_pwd_dlg, &KeyPassphraseDialog::passphrase_submitted, this, [this, key_id](QByteArrayView passphrase) {
+ AuthResponse response;
+ ResponseResult result = unlock_and_auth(key_id, passphrase, m_challenge.challenge, m_ao_app->ex_auth_username.toUtf8(), response);
+ if (result == ResponseResult::success)
+ {
+ m_ao_app->send_ex_message(serializeAuthResponse(response));
+ if (m_mode == FlowMode::Default)
+ {
+ m_key_dlg->accept();
+ m_ao_app->saved_auth.insert(m_ao_app->m_serverdata.m_server_hostname.toUtf8(), m_ao_app->ex_auth_username.toUtf8(), key_id);
+ }
+ else
+ {
+ m_pwd_dlg->accept();
+ }
+ deleteLater();
+ }
+ else
+ {
+ m_pwd_dlg->display_error(result);
+ }
+ });
+ connect(m_pwd_dlg, &QDialog::rejected, this, [this] {
+ if (m_mode == FlowMode::Saved)
+ {
+ m_ao_app->saved_auth.remove(m_ao_app->m_serverdata.m_server_hostname.toUtf8(), m_ao_app->ex_auth_username.toUtf8());
+ m_mode = FlowMode::Default;
+
+ m_key_dlg = new KeySelectDialog(&m_ao_app->keyring_model, m_ao_app->ex_auth_username);
+ connect(m_key_dlg, &KeySelectDialog::key_selected, this, &AuthFlow::on_key_selected);
+ m_key_dlg->open();
+ }
+ });
+ m_pwd_dlg->open();
+}
diff --git a/src/auth_flow.h b/src/auth_flow.h
new file mode 100644
index 0000000..942e1aa
--- /dev/null
+++ b/src/auth_flow.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <QString>
+
+#include "aoapplication.h"
+#include "ext_packet.h"
+#include "keyring.h"
+
+class KeySelectDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit KeySelectDialog(KeyringModel *model, QStringView username, QWidget *parent = nullptr);
+
+private:
+ KeyringModel *m_model;
+
+signals:
+ void key_selected(QByteArrayView key_id, QStringView key_name);
+};
+
+class KeyPassphraseDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit KeyPassphraseDialog(QStringView key_name, QWidget *parent = nullptr);
+ void display_error(ResponseResult error) const;
+
+private:
+ QLabel *m_err_lbl;
+ QLineEdit *m_pw_line;
+
+signals:
+ void passphrase_submitted(QByteArrayView passphrase);
+};
+
+// The only reason this inherits QObject is so I can call deleteLater()
+// when the authentication finishes. Auth flow involves coordinating two
+// asynchronous dialogs emitting signals, so I'd rather let Qt's event loop
+// clean everything up to be safe.
+class AuthFlow : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit AuthFlow(AOApplication *ao_app, const AuthChallenge &challenge, QWidget *parent = nullptr);
+
+private:
+ enum class FlowMode
+ {
+ Default,
+ Saved
+ };
+
+ FlowMode m_mode;
+ AOApplication *m_ao_app;
+ AuthChallenge m_challenge;
+ KeySelectDialog *m_key_dlg;
+ KeyPassphraseDialog *m_pwd_dlg;
+
+ void on_key_selected(QByteArrayView key_id, QStringView key_name);
+};
+
+void start_auth_flow(AOApplication *ao_app, QString username);
diff --git a/src/courtroom.cpp b/src/courtroom.cpp
index 1eaa747..a2dca24 100644
--- a/src/courtroom.cpp
+++ b/src/courtroom.cpp
@@ -5118,6 +5118,19 @@ void Courtroom::on_ooc_return_pressed()
ui_ooc_chat_message->clear();
return;
}
+ // Only works on CSDWASASH.
+ else if (ooc_message.startsWith("/auth"))
+ {
+ QStringList command = ooc_message.split(" ", Qt::SkipEmptyParts);
+ if (command.size() != 2)
+ {
+ append_server_chatmessage("CLIENT", "Usage: /auth <username>", "1");
+ return;
+ }
+ start_auth_flow(ao_app, command[1]);
+ ui_ooc_chat_message->clear();
+ return;
+ }
QStringList packet_contents;
packet_contents.append(ui_ooc_chat_name->text());
diff --git a/src/courtroom.h b/src/courtroom.h
index df8f30c..4d32ab4 100644
--- a/src/courtroom.h
+++ b/src/courtroom.h
@@ -16,6 +16,7 @@
#include "aosfxplayer.h"
#include "aotextarea.h"
#include "aotextboxwidgets.h"
+#include "auth_flow.h"
#include "chatlogpiece.h"
#include "datatypes.h"
#include "debug_functions.h"
diff --git a/src/main.cpp b/src/main.cpp
index e998eed..0bc4e80 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -104,6 +104,7 @@ int main(int argc, char *argv[])
return EXIT_FAILURE;
}
main_app.keyring_model.load_keys();
+ main_app.saved_auth.load();
main_app.construct_lobby();
main_app.net_manager->get_server_list();
diff --git a/src/packet_distribution.cpp b/src/packet_distribution.cpp
index 2ff35ce..90a6758 100644
--- a/src/packet_distribution.cpp
+++ b/src/packet_distribution.cpp
@@ -133,6 +133,7 @@ void AOApplication::server_packet_received(AOPacket packet)
server_name = info.name;
server_address = QString("%1:%2").arg(info.address, QString::number(info.port));
window_title = server_name;
+ m_serverdata.m_server_hostname = info.address;
}
break;
@@ -145,6 +146,7 @@ void AOApplication::server_packet_received(AOPacket packet)
server_name = info.name;
server_address = QString("%1:%2").arg(info.address, QString::number(info.port));
window_title = server_name;
+ m_serverdata.m_server_hostname = info.address;
}
}
break;
diff --git a/src/saved_auth.cpp b/src/saved_auth.cpp
new file mode 100644
index 0000000..aa1c696
--- /dev/null
+++ b/src/saved_auth.cpp
@@ -0,0 +1,124 @@
+#include "saved_auth.h"
+
+#include <QDir>
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+#include "file_functions.h"
+
+// I wish I could use INI for settings to let users edit it manually if they
+// wanted to, like ~/.ssh/config. Unfortunately, due to adherence to idiotic
+// conventions, INI keys are case-insensitive on Windows, while the usernames
+// aren't. Rather than defining my own stupid encoding to work around this
+// issue (thus ruining convenience I originally strived for), I'll ditch INI
+// altogether. I could use CBOR for consistency, but I'm using JSON because
+// it's supposed to be manually editable, and I don't want to complicate
+// things by creating a new settings format.
+bool SavedAuth::load()
+{
+ QFile saved_auth_file(QDir(get_app_path()).filePath("saved_auth.json"));
+ if (!saved_auth_file.open(QIODevice::ReadOnly))
+ {
+ return false;
+ }
+ QByteArray raw_data = saved_auth_file.readAll();
+ saved_auth_file.close();
+
+ QJsonParseError err;
+ QJsonDocument doc = QJsonDocument::fromJson(raw_data, &err);
+ if (err.error != QJsonParseError::NoError || !doc.isObject())
+ {
+ return false;
+ }
+
+ m_table.clear();
+ QJsonObject root = doc.object();
+ for (auto host_it = root.begin(); host_it != root.end(); ++host_it)
+ {
+ if (!host_it.value().isObject())
+ {
+ continue;
+ }
+
+ QByteArray host = host_it.key().toUtf8();
+ QJsonObject users = host_it.value().toObject();
+
+ for (auto user_it = users.begin(); user_it != users.end(); ++user_it)
+ {
+ if (!user_it.value().isString())
+ {
+ continue;
+ }
+
+ QByteArray user = user_it.key().toUtf8();
+ QByteArray saved_key = QByteArray::fromBase64(user_it.value().toString().toUtf8());
+
+ m_table[host + '\0' + user] = saved_key;
+ }
+ }
+
+ return true;
+}
+
+QByteArray SavedAuth::lookup(QByteArrayView host, QByteArrayView user) const
+{
+ return m_table.value(flatten_key(host, user));
+}
+
+void SavedAuth::insert(QByteArrayView host, QByteArrayView user, QByteArrayView saved_key)
+{
+ m_table[flatten_key(host, user)] = QByteArray(saved_key);
+ save();
+}
+
+void SavedAuth::remove(QByteArrayView host, QByteArrayView user)
+{
+ m_table.remove(flatten_key(host, user));
+ save();
+}
+
+bool SavedAuth::save() const
+{
+ QJsonObject root;
+
+ for (auto it = m_table.begin(); it != m_table.end(); ++it)
+ {
+ const QByteArray &flat = it.key();
+ const QByteArray &key_id = it.value();
+
+ int sep = flat.indexOf('\0');
+ if (sep < 0)
+ {
+ continue;
+ }
+
+ QString host = QString::fromUtf8(flat.left(sep));
+ QString user = QString::fromUtf8(flat.mid(sep + 1));
+
+ QJsonObject host_obj = root.value(host).toObject();
+
+ host_obj[user] = QString::fromUtf8(key_id.toBase64(QByteArray::OmitTrailingEquals));
+
+ root[host] = host_obj;
+ }
+
+ QJsonDocument doc(root);
+
+ QFile f(QDir(get_app_path()).filePath("saved_auth.json"));
+ if (!f.open(QIODevice::WriteOnly))
+ return false;
+
+ f.write(doc.toJson(QJsonDocument::Indented));
+ return true;
+}
+
+QByteArray SavedAuth::flatten_key(QByteArrayView host, QByteArrayView user) const
+{
+ QByteArray key;
+ key.reserve(host.size() + 1 + user.size());
+ key.append(host);
+ key.append('\0');
+ key.append(user);
+ return key;
+}
diff --git a/src/saved_auth.h b/src/saved_auth.h
new file mode 100644
index 0000000..d37a8fd
--- /dev/null
+++ b/src/saved_auth.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <QHash>
+
+class SavedAuth
+{
+public:
+ bool load();
+ QByteArray lookup(QByteArrayView host, QByteArrayView user) const;
+ void insert(QByteArrayView host, QByteArrayView user, QByteArrayView key);
+ void remove(QByteArrayView host, QByteArrayView user);
+
+private:
+ QHash<QByteArray, QByteArray> m_table;
+ bool save() const;
+ QByteArray flatten_key(QByteArrayView host, QByteArrayView user) const;
+};
diff --git a/src/serverdata.h b/src/serverdata.h
index 9935185..4f4e572 100644
--- a/src/serverdata.h
+++ b/src/serverdata.h
@@ -134,6 +134,9 @@ public:
*/
void set_asset_url(const QString &f_asset_url);
+ // There's no need for accessors here.
+ QString m_server_hostname;
+
private:
/// The features available on the server. Determines what
QStringList m_features;