summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorDeepBlueV7.X <nicolas.werner@hotmail.de>2021-10-09 23:35:09 +0000
committerGitHub <noreply@github.com>2021-10-09 23:35:09 +0000
commit281d764aa3ce0ea55536a6356e1ed3511aaff6f4 (patch)
treeaa8c88bdf2788c84053e7009adafea4c1b4a4c75 /src
parentMerge pull request #743 from LorenDB/qmlLogout (diff)
parentSupport bootstrapping crosssigning (diff)
downloadnheko-281d764aa3ce0ea55536a6356e1ed3511aaff6f4.tar.xz
Merge pull request #755 from Nheko-Reborn/bootstrapping
Support bootstrapping crosssigning
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp12
-rw-r--r--src/Cache_p.h1
-rw-r--r--src/SelfVerificationStatus.cpp249
-rw-r--r--src/SelfVerificationStatus.h43
-rw-r--r--src/timeline/TimelineViewManager.cpp44
-rw-r--r--src/ui/UIA.cpp136
-rw-r--r--src/ui/UIA.h40
7 files changed, 495 insertions, 30 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp
index ee0ca0c2..ea3dd525 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -201,6 +201,18 @@ Cache::Cache(const QString &userId, QObject *parent)
 {
     setup();
     connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
+    connect(
+      this,
+      &Cache::verificationStatusChanged,
+      this,
+      [this](const std::string &u) {
+          if (u == localUserId_.toStdString()) {
+              auto status = verificationStatus(u);
+              if (status.unverified_device_count || !status.user_verified)
+                  emit selfUnverified();
+          }
+      },
+      Qt::QueuedConnection);
 }
 
 void
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 52375d38..f7db77d4 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -310,6 +310,7 @@ signals:
     void removeNotification(const QString &room_id, const QString &event_id);
     void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
     void verificationStatusChanged(const std::string &userid);
+    void selfUnverified();
     void secretChanged(const std::string name);
 
 private:
diff --git a/src/SelfVerificationStatus.cpp b/src/SelfVerificationStatus.cpp
new file mode 100644
index 00000000..d75a2109
--- /dev/null
+++ b/src/SelfVerificationStatus.cpp
@@ -0,0 +1,249 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "SelfVerificationStatus.h"
+
+#include "Cache_p.h"
+#include "Logging.h"
+#include "MainWindow.h"
+#include "MatrixClient.h"
+#include "Olm.h"
+#include "ui/UIA.h"
+
+#include <mtx/responses/common.hpp>
+
+SelfVerificationStatus::SelfVerificationStatus(QObject *o)
+  : QObject(o)
+{
+    connect(MainWindow::instance(), &MainWindow::reload, this, [this] {
+        connect(cache::client(),
+                &Cache::selfUnverified,
+                this,
+                &SelfVerificationStatus::invalidate,
+                Qt::UniqueConnection);
+        invalidate();
+    });
+}
+
+void
+SelfVerificationStatus::setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup)
+{
+    nhlog::db()->info("Clicked setup crossigning");
+
+    auto xsign_keys = olm::client()->create_crosssigning_keys();
+
+    if (!xsign_keys) {
+        nhlog::crypto()->critical("Failed to setup cross-signing keys!");
+        emit setupFailed(tr("Failed to create keys for cross-signing!"));
+        return;
+    }
+
+    cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_master,
+                                 xsign_keys->private_master_key);
+    cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_self_signing,
+                                 xsign_keys->private_self_signing_key);
+    cache::client()->storeSecret(mtx::secret_storage::secrets::cross_signing_user_signing,
+                                 xsign_keys->private_user_signing_key);
+
+    std::optional<mtx::crypto::OlmClient::OnlineKeyBackupSetup> okb;
+    if (useOnlineKeyBackup) {
+        okb = olm::client()->create_online_key_backup(xsign_keys->private_master_key);
+        if (!okb) {
+            nhlog::crypto()->critical("Failed to setup online key backup!");
+            emit setupFailed(tr("Failed to create keys for online key backup!"));
+            return;
+        }
+
+        cache::client()->storeSecret(
+          mtx::secret_storage::secrets::megolm_backup_v1,
+          mtx::crypto::bin2base64(mtx::crypto::to_string(okb->privateKey)));
+
+        http::client()->post_backup_version(
+          okb->backupVersion.algorithm,
+          okb->backupVersion.auth_data,
+          [](const mtx::responses::Version &v, mtx::http::RequestErr e) {
+              if (e) {
+                  nhlog::net()->error("error setting up online key backup: {} {} {} {}",
+                                      e->parse_error,
+                                      e->status_code,
+                                      e->error_code,
+                                      e->matrix_error.error);
+              } else {
+                  nhlog::crypto()->info("Set up online key backup: '{}'", v.version);
+              }
+          });
+    }
+
+    std::optional<mtx::crypto::OlmClient::SSSSSetup> ssss;
+    if (useSSSS) {
+        ssss = olm::client()->create_ssss_key(password.toStdString());
+        if (!ssss) {
+            nhlog::crypto()->critical("Failed to setup secure server side secret storage!");
+            emit setupFailed(tr("Failed to create keys secure server side secret storage!"));
+            return;
+        }
+
+        auto master      = mtx::crypto::PkSigning::from_seed(xsign_keys->private_master_key);
+        nlohmann::json j = ssss->keyDescription;
+        j.erase("signatures");
+        ssss->keyDescription
+          .signatures[http::client()->user_id().to_string()]["ed25519:" + master.public_key()] =
+          master.sign(j.dump());
+
+        http::client()->upload_secret_storage_key(
+          ssss->keyDescription.name, ssss->keyDescription, [](mtx::http::RequestErr) {});
+        http::client()->set_secret_storage_default_key(ssss->keyDescription.name,
+                                                       [](mtx::http::RequestErr) {});
+
+        auto uploadSecret = [ssss](const std::string &key_name, const std::string &secret) {
+            mtx::secret_storage::Secret s;
+            s.encrypted[ssss->keyDescription.name] =
+              mtx::crypto::encrypt(secret, ssss->privateKey, key_name);
+            http::client()->upload_secret_storage_secret(
+              key_name, s, [key_name](mtx::http::RequestErr) {
+                  nhlog::crypto()->info("Uploaded secret: {}", key_name);
+              });
+        };
+
+        uploadSecret(mtx::secret_storage::secrets::cross_signing_master,
+                     xsign_keys->private_master_key);
+        uploadSecret(mtx::secret_storage::secrets::cross_signing_self_signing,
+                     xsign_keys->private_self_signing_key);
+        uploadSecret(mtx::secret_storage::secrets::cross_signing_user_signing,
+                     xsign_keys->private_user_signing_key);
+
+        if (okb)
+            uploadSecret(mtx::secret_storage::secrets::megolm_backup_v1,
+                         mtx::crypto::bin2base64(mtx::crypto::to_string(okb->privateKey)));
+    }
+
+    mtx::requests::DeviceSigningUpload device_sign{};
+    device_sign.master_key       = xsign_keys->master_key;
+    device_sign.self_signing_key = xsign_keys->self_signing_key;
+    device_sign.user_signing_key = xsign_keys->user_signing_key;
+    http::client()->device_signing_upload(
+      device_sign,
+      UIA::instance()->genericHandler(tr("Encryption Setup")),
+      [this, ssss, xsign_keys](mtx::http::RequestErr e) {
+          if (e) {
+              nhlog::crypto()->critical("Failed to upload cross signing keys: {}",
+                                        e->matrix_error.error);
+
+              emit setupFailed(tr("Encryption setup failed: %1")
+                                 .arg(QString::fromStdString(e->matrix_error.error)));
+              return;
+          }
+          nhlog::crypto()->info("Crosssigning keys uploaded!");
+
+          auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
+          if (deviceKeys) {
+              auto myKey = deviceKeys->device_keys.at(http::client()->device_id());
+              if (myKey.user_id == http::client()->user_id().to_string() &&
+                  myKey.device_id == http::client()->device_id() &&
+                  myKey.keys["ed25519:" + http::client()->device_id()] ==
+                    olm::client()->identity_keys().ed25519 &&
+                  myKey.keys["curve25519:" + http::client()->device_id()] ==
+                    olm::client()->identity_keys().curve25519) {
+                  json j = myKey;
+                  j.erase("signatures");
+                  j.erase("unsigned");
+
+                  auto ssk =
+                    mtx::crypto::PkSigning::from_seed(xsign_keys->private_self_signing_key);
+                  myKey.signatures[http::client()->user_id().to_string()]
+                                  ["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());
+                  mtx::requests::KeySignaturesUpload req;
+                  req.signatures[http::client()->user_id().to_string()]
+                                [http::client()->device_id()] = myKey;
+
+                  http::client()->keys_signatures_upload(
+                    req,
+                    [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) {
+                        if (err) {
+                            nhlog::net()->error("failed to upload signatures: {},{}",
+                                                mtx::errors::to_string(err->matrix_error.errcode),
+                                                static_cast<int>(err->status_code));
+                        }
+
+                        for (const auto &[user_id, tmp] : res.errors)
+                            for (const auto &[key_id, e] : tmp)
+                                nhlog::net()->error("signature error for user {} and key "
+                                                    "id {}: {}, {}",
+                                                    user_id,
+                                                    key_id,
+                                                    mtx::errors::to_string(e.errcode),
+                                                    e.error);
+                    });
+              }
+          }
+
+          if (ssss) {
+              auto k = QString::fromStdString(mtx::crypto::key_to_recoverykey(ssss->privateKey));
+
+              QString r;
+              for (int i = 0; i < k.size(); i += 4)
+                  r += k.mid(i, 4) + " ";
+
+              emit showRecoveryKey(r.trimmed());
+          } else {
+              emit setupCompleted();
+          }
+      });
+}
+
+void
+SelfVerificationStatus::verifyMasterKey()
+{
+    nhlog::db()->info("Clicked verify master key");
+}
+
+void
+SelfVerificationStatus::verifyUnverifiedDevices()
+{
+    nhlog::db()->info("Clicked verify unverified devices");
+}
+
+void
+SelfVerificationStatus::invalidate()
+{
+    nhlog::db()->info("Invalidating self verification status");
+    auto keys = cache::client()->userKeys(http::client()->user_id().to_string());
+    if (!keys) {
+        cache::client()->query_keys(http::client()->user_id().to_string(),
+                                    [](const UserKeyCache &, mtx::http::RequestErr) {});
+        return;
+    }
+
+    if (keys->master_keys.keys.empty()) {
+        if (status_ != SelfVerificationStatus::NoMasterKey) {
+            this->status_ = SelfVerificationStatus::NoMasterKey;
+            emit statusChanged();
+        }
+        return;
+    }
+
+    auto verifStatus = cache::client()->verificationStatus(http::client()->user_id().to_string());
+
+    if (!verifStatus.user_verified) {
+        if (status_ != SelfVerificationStatus::UnverifiedMasterKey) {
+            this->status_ = SelfVerificationStatus::UnverifiedMasterKey;
+            emit statusChanged();
+        }
+        return;
+    }
+
+    if (verifStatus.unverified_device_count > 0) {
+        if (status_ != SelfVerificationStatus::UnverifiedDevices) {
+            this->status_ = SelfVerificationStatus::UnverifiedDevices;
+            emit statusChanged();
+        }
+        return;
+    }
+
+    if (status_ != SelfVerificationStatus::AllVerified) {
+        this->status_ = SelfVerificationStatus::AllVerified;
+        emit statusChanged();
+        return;
+    }
+}
diff --git a/src/SelfVerificationStatus.h b/src/SelfVerificationStatus.h
new file mode 100644
index 00000000..8cb54df6
--- /dev/null
+++ b/src/SelfVerificationStatus.h
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QObject>
+
+class SelfVerificationStatus : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(Status status READ status NOTIFY statusChanged)
+
+public:
+    SelfVerificationStatus(QObject *o = nullptr);
+    enum Status
+    {
+        AllVerified,
+        NoMasterKey,
+        UnverifiedMasterKey,
+        UnverifiedDevices,
+    };
+    Q_ENUM(Status)
+
+    Q_INVOKABLE void setupCrosssigning(bool useSSSS, QString password, bool useOnlineKeyBackup);
+    Q_INVOKABLE void verifyMasterKey();
+    Q_INVOKABLE void verifyUnverifiedDevices();
+
+    Status status() const { return status_; }
+
+signals:
+    void statusChanged();
+    void setupCompleted();
+    void showRecoveryKey(QString key);
+    void setupFailed(QString message);
+
+public slots:
+    void invalidate();
+
+private:
+    Status status_ = AllVerified;
+};
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 8a33dc2b..df8210d3 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -29,6 +29,7 @@
 #include "ReadReceiptsModel.h"
 #include "RoomDirectoryModel.h"
 #include "RoomsModel.h"
+#include "SelfVerificationStatus.h"
 #include "SingleImagePackModel.h"
 #include "UserSettingsPage.h"
 #include "UsersModel.h"
@@ -40,6 +41,7 @@
 #include "ui/NhekoCursorShape.h"
 #include "ui/NhekoDropArea.h"
 #include "ui/NhekoGlobalObject.h"
+#include "ui/UIA.h"
 
 Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
 Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
@@ -212,18 +214,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
       "ReadReceiptsProxy needs to be instantiated on the C++ side");
 
     static auto self = this;
-    qmlRegisterSingletonType<MainWindow>(
-      "im.nheko", 1, 0, "MainWindow", [](QQmlEngine *, QJSEngine *) -> QObject * {
-          auto ptr = MainWindow::instance();
-          QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-          return ptr;
-      });
-    qmlRegisterSingletonType<TimelineViewManager>(
-      "im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
-          auto ptr = self;
-          QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-          return ptr;
-      });
+    qmlRegisterSingletonInstance("im.nheko", 1, 0, "MainWindow", MainWindow::instance());
+    qmlRegisterSingletonInstance("im.nheko", 1, 0, "TimelineManager", self);
+    qmlRegisterSingletonInstance("im.nheko", 1, 0, "UIA", UIA::instance());
     qmlRegisterSingletonType<RoomlistModel>(
       "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
           auto ptr = new FilteredRoomlistModel(self->rooms_);
@@ -238,24 +231,11 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                   &FilteredRoomlistModel::updateHiddenTagsAndSpaces);
           return ptr;
       });
-    qmlRegisterSingletonType<RoomlistModel>(
-      "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * {
-          auto ptr = self->communities_;
-          QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-          return ptr;
-      });
-    qmlRegisterSingletonType<UserSettings>(
-      "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
-          auto ptr = ChatPage::instance()->userSettings().data();
-          QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-          return ptr;
-      });
-    qmlRegisterSingletonType<CallManager>(
-      "im.nheko", 1, 0, "CallManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
-          auto ptr = ChatPage::instance()->callManager();
-          QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-          return ptr;
-      });
+    qmlRegisterSingletonInstance("im.nheko", 1, 0, "Communities", self->communities_);
+    qmlRegisterSingletonInstance(
+      "im.nheko", 1, 0, "Settings", ChatPage::instance()->userSettings().data());
+    qmlRegisterSingletonInstance(
+      "im.nheko", 1, 0, "CallManager", ChatPage::instance()->callManager());
     qmlRegisterSingletonType<Clipboard>(
       "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
           return new Clipboard();
@@ -264,6 +244,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
       "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
           return new Nheko();
       });
+    qmlRegisterSingletonType<SelfVerificationStatus>(
+      "im.nheko", 1, 0, "SelfVerificationStatus", [](QQmlEngine *, QJSEngine *) -> QObject * {
+          return new SelfVerificationStatus();
+      });
 
     qRegisterMetaType<mtx::events::collections::TimelineEvents>();
     qRegisterMetaType<std::vector<DeviceInfo>>();
diff --git a/src/ui/UIA.cpp b/src/ui/UIA.cpp
new file mode 100644
index 00000000..29161382
--- /dev/null
+++ b/src/ui/UIA.cpp
@@ -0,0 +1,136 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "UIA.h"
+
+#include <algorithm>
+
+#include <QInputDialog>
+#include <QTimer>
+
+#include "Logging.h"
+#include "MainWindow.h"
+#include "dialogs/FallbackAuth.h"
+#include "dialogs/ReCaptcha.h"
+
+UIA *
+UIA::instance()
+{
+    static UIA uia;
+    return &uia;
+}
+
+mtx::http::UIAHandler
+UIA::genericHandler(QString context)
+{
+    return mtx::http::UIAHandler([this, context](const mtx::http::UIAHandler &h,
+                                                 const mtx::user_interactive::Unauthorized &u) {
+        QTimer::singleShot(0, this, [this, h, u, context]() {
+            this->currentHandler = h;
+            this->currentStatus  = u;
+            this->title_         = context;
+            emit titleChanged();
+
+            std::vector<mtx::user_interactive::Flow> flows = u.flows;
+
+            nhlog::ui()->info("Completed stages: {}", u.completed.size());
+
+            if (!u.completed.empty()) {
+                // Get rid of all flows which don't start with the sequence of
+                // stages that have already been completed.
+                flows.erase(std::remove_if(flows.begin(),
+                                           flows.end(),
+                                           [completed_stages = u.completed](auto flow) {
+                                               if (completed_stages.size() > flow.stages.size())
+                                                   return true;
+                                               for (size_t f = 0; f < completed_stages.size(); f++)
+                                                   if (completed_stages[f] != flow.stages[f])
+                                                       return true;
+                                               return false;
+                                           }),
+                            flows.end());
+            }
+
+            if (flows.empty()) {
+                nhlog::ui()->error("No available registration flows!");
+                return;
+            }
+
+            auto current_stage = flows.front().stages.at(u.completed.size());
+
+            if (current_stage == mtx::user_interactive::auth_types::password) {
+                emit password();
+            } else if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
+                auto captchaDialog =
+                  new dialogs::ReCaptcha(QString::fromStdString(u.session), MainWindow::instance());
+                captchaDialog->setWindowTitle(context);
+
+                connect(
+                  captchaDialog, &dialogs::ReCaptcha::confirmation, this, [captchaDialog, h, u]() {
+                      captchaDialog->close();
+                      captchaDialog->deleteLater();
+                      h.next(mtx::user_interactive::Auth{u.session,
+                                                         mtx::user_interactive::auth::Fallback{}});
+                  });
+
+                // connect(
+                //  captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred);
+
+                QTimer::singleShot(0, this, [captchaDialog]() { captchaDialog->show(); });
+
+            } else if (current_stage == mtx::user_interactive::auth_types::dummy) {
+                h.next(
+                  mtx::user_interactive::Auth{u.session, mtx::user_interactive::auth::Dummy{}});
+
+            } else if (current_stage == mtx::user_interactive::auth_types::registration_token) {
+                bool ok;
+                QString token =
+                  QInputDialog::getText(MainWindow::instance(),
+                                        context,
+                                        tr("Please enter a valid registration token."),
+                                        QLineEdit::Normal,
+                                        QString(),
+                                        &ok);
+
+                if (ok) {
+                    h.next(mtx::user_interactive::Auth{
+                      u.session,
+                      mtx::user_interactive::auth::RegistrationToken{token.toStdString()}});
+                } else {
+                    // emit errorOccurred();
+                }
+            } else {
+                // use fallback
+                auto dialog = new dialogs::FallbackAuth(QString::fromStdString(current_stage),
+                                                        QString::fromStdString(u.session),
+                                                        MainWindow::instance());
+                dialog->setWindowTitle(context);
+
+                connect(dialog, &dialogs::FallbackAuth::confirmation, this, [h, u, dialog]() {
+                    dialog->close();
+                    dialog->deleteLater();
+                    h.next(mtx::user_interactive::Auth{u.session,
+                                                       mtx::user_interactive::auth::Fallback{}});
+                });
+
+                // connect(dialog, &dialogs::FallbackAuth::cancel, this,
+                // &RegisterPage::errorOccurred);
+
+                dialog->show();
+            }
+        });
+    });
+}
+
+void
+UIA::continuePassword(QString password)
+{
+    mtx::user_interactive::auth::Password p{};
+    p.identifier_type = mtx::user_interactive::auth::Password::UserId;
+    p.password        = password.toStdString();
+    p.identifier_user = http::client()->user_id().to_string();
+
+    if (currentHandler)
+        currentHandler->next(mtx::user_interactive::Auth{currentStatus.session, p});
+}
diff --git a/src/ui/UIA.h b/src/ui/UIA.h
new file mode 100644
index 00000000..fb047451
--- /dev/null
+++ b/src/ui/UIA.h
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QObject>
+
+#include <MatrixClient.h>
+
+class UIA : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QString title READ title NOTIFY titleChanged)
+
+public:
+    static UIA *instance();
+
+    UIA(QObject *parent = nullptr)
+      : QObject(parent)
+    {}
+
+    mtx::http::UIAHandler genericHandler(QString context);
+
+    QString title() const { return title_; }
+
+public slots:
+    void continuePassword(QString password);
+
+signals:
+    void password();
+
+    void titleChanged();
+
+private:
+    std::optional<mtx::http::UIAHandler> currentHandler;
+    mtx::user_interactive::Unauthorized currentStatus;
+    QString title_;
+};