Page MenuHome GnuPG

No OneTemporary

diff --git a/src/gpgolkeyadder/gpgolkeyadder-options.h b/src/gpgolkeyadder/gpgolkeyadder-options.h
index 038e411..f4de06d 100644
--- a/src/gpgolkeyadder/gpgolkeyadder-options.h
+++ b/src/gpgolkeyadder/gpgolkeyadder-options.h
@@ -1,37 +1,43 @@
#ifndef GPGOLKEYADDER_OPTIONS
#define GPGOLKEYADDER_OPTIONS
/* Copyright (C) 2018 by Intevation GmbH <info@intevation.de>
*
* This file is Free Software under the GNU GPL (v>=2)
* and comes with ABSOLUTELY NO WARRANTY!
* See LICENSE.txt for details.
*/
#include <QCommandLineParser>
#include <QList>
/** @file Commandline options*/
static void options(QCommandLineParser &parser)
{
QList<QCommandLineOption> options;
options
<< QCommandLineOption(QStringList() << QStringLiteral("debug"),
QStringLiteral("Print debug output."))
+ << QCommandLineOption(QStringList() << QStringLiteral("cms"),
+ QStringLiteral("Add S/MIME settings"))
<< QCommandLineOption(QStringLiteral("hwnd"),
QStringLiteral("Parent Window"),
QStringLiteral("windows window handle"))
+ << QCommandLineOption(QStringList() << QStringLiteral("sign"),
+ QStringLiteral("Always sign"))
+ << QCommandLineOption(QStringList() << QStringLiteral("encrypt"),
+ QStringLiteral("Always enccrypt"))
<< QCommandLineOption(QStringLiteral("username"),
QStringLiteral("Name"),
QStringLiteral("username"))
<< QCommandLineOption(QStringLiteral("lang"),
QStringLiteral("Language"),
QStringLiteral("Language to be used e.g. de_DE"));
for (const auto &opt: options) {
parser.addOption(opt);
}
parser.addVersionOption();
parser.addHelpOption();
}
#endif
diff --git a/src/gpgolkeyadder/gpgolkeyadder.cpp b/src/gpgolkeyadder/gpgolkeyadder.cpp
index 6efc183..4182a2f 100644
--- a/src/gpgolkeyadder/gpgolkeyadder.cpp
+++ b/src/gpgolkeyadder/gpgolkeyadder.cpp
@@ -1,249 +1,536 @@
/* Copyright (C) 2018 by Intevation GmbH <info@intevation.de>
*
* This file is Free Software under the GNU GPL (v>=2)
* and comes with ABSOLUTELY NO WARRANTY!
* See LICENSE.txt for details.
*/
#include "gpgolkeyadder.h"
#include "w32-gettext.h"
#include "w32-util.h"
#include "w32-qt-util.h"
#include "stdinreader.h"
#include <QLabel>
#include <QWidget>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QCommandLineParser>
+#include <QCheckBox>
#include <QDebug>
#include <QTextEdit>
#include <QFont>
#include <QFontMetrics>
#include <QMessageBox>
#include <QDateTime>
#include <QLocale>
-
+#include <QTabWidget>
+#include <QToolTip>
+#include <QThread>
+#include <QProgressDialog>
+
+#include <gpgme++/context.h>
+#include <gpgme++/keylistresult.h>
+#include <gpgme++/importresult.h>
#include <gpgme++/data.h>
#include <gpgme++/key.h>
#include <iostream>
+namespace
+{
+
+static char **vector_to_cArray(const std::vector<std::string> &vec)
+{
+ char ** ret = (char**) malloc (sizeof (char*) * (vec.size() + 1));
+ for (size_t i = 0; i < vec.size(); i++)
+ {
+ ret[i] = strdup (vec[i].c_str());
+ }
+ ret[vec.size()] = NULL;
+ return ret;
+}
+
+static void release_cArray (char **carray)
+{
+ if (carray)
+ {
+ for (int idx = 0; carray[idx]; idx++)
+ {
+ free (carray[idx]);
+ }
+ free (carray);
+ }
+}
+
+class CMSImportThread: public QThread
+{
+ Q_OBJECT
+
+ public:
+ explicit CMSImportThread(const GpgME::Data &data):
+ mData(data)
+ {
+ }
+
+ std::vector<GpgME::Key> certs()
+ {
+ return mCerts;
+ }
+
+ protected:
+ void run() override
+ {
+ auto ctx = GpgME::Context::create(GpgME::CMS);
+
+ if (!ctx) {
+ qDebug () << "No ctx";
+ return;
+ }
+
+ const auto result = ctx->importKeys(mData);
+ std::vector<std::string> fingerprints;
+
+ for (const auto import: result.imports()) {
+ if (import.error()){
+ qDebug() << "Error importing:" << import.error().asString();
+ continue;
+ }
+ const char *fpr = import.fingerprint ();
+ if (!fpr)
+ {
+ qDebug() << "Import with no fpr.";
+ continue;
+ }
+ fingerprints.push_back (fpr);
+ qDebug () << "imported: " << fpr;
+ }
+ if (!fingerprints.size()) {
+ qDebug () << "Nothing imported";
+ return;
+ }
+ ctx = GpgME::Context::create(GpgME::CMS);
+ ctx->setKeyListMode (GpgME::KeyListMode::Local |
+ GpgME::KeyListMode::Validate);
+
+ GpgME::Error err;
+ char **patterns = vector_to_cArray(fingerprints);
+ err = ctx->startKeyListing((const char**)patterns, false);
+ release_cArray(patterns);
+ if (err) {
+ qDebug() << "Failed to start keylisting err:" << err.asString();
+ return;
+ }
+
+ while (!err) {
+ const auto key = ctx->nextKey(err);
+ if (err || key.isNull()) {
+ break;
+ }
+ mCerts.push_back(key);
+ }
+ return;
+ }
+
+ private:
+ GpgME::Data mData;
+ std::vector<GpgME::Key> mCerts;
+};
+} // Namespace
+
+
GpgOLKeyAdder::GpgOLKeyAdder(const QCommandLineParser &parser):
QDialog(nullptr),
- mEdit(new QTextEdit)
+ mEdit(new QTextEdit),
+ mCMSEdit(new QTextEdit),
+ mAlwaysSec(new QCheckBox)
{
setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint));
mName = parser.value(QStringLiteral("username"));
+ mShowCMS = parser.isSet(QStringLiteral("cms"));
- setWindowTitle(_("GpgOL") + QStringLiteral(" - ") + _("Configure key for:") +
+ setWindowTitle(_("GpgOL") + QStringLiteral(" - ") + _("Settings for:") +
QStringLiteral(" ") + mName);
setWindowIcon(QIcon(":/gpgol-icon.svg"));
const auto hwnd = parser.value(QStringLiteral("hwnd"));
if (!hwnd.isEmpty()) {
bool ok;
WId id = (WId) hwnd.toInt(&ok);
if (!ok) {
qDebug() << "invalid hwnd value";
} else {
W32::setupForeignParent(id, this, true);
setModal(true);
}
}
+ mAlwaysSec->setChecked(parser.isSet("encrypt") && parser.isSet("sign"));
+
setupGUI();
// Deletes itself
auto reader = new StdinReader;
connect (reader, &StdinReader::stdinRead, this, [this] (const QByteArray &data) {
if (data.size() > 1) {
- mEdit->setPlainText(QString::fromUtf8(data));
+ handleInput(data);
}
});
reader->start();
}
+void GpgOLKeyAdder::handleInput(const QByteArray &data) {
+ const auto stringData = QString::fromUtf8(data);
+ const auto splitData = stringData.split("BEGIN CMS DATA\n");
+ if (splitData.size() != 2) {
+ qDebug() << "Failed to split data. Old GpgOL Version?";
+ mEdit->setPlainText(stringData);
+ } else {
+ mEdit->setPlainText(splitData[0]);
+ mCMSEdit->setPlainText(splitData[1]);
+ }
+}
+
void GpgOLKeyAdder::setupGUI()
{
- /* Setup Edit */
+ /* Setup Edits */
auto fixedFont = QFont("Monospace", 10);
fixedFont.setStyleHint(QFont::TypeWriter);
- mEdit->setFont(fixedFont);
resize(QFontMetrics(fixedFont).averageCharWidth() * 80,
QFontMetrics(fixedFont).height() * 30);
+
+ mEdit->setFont(fixedFont);
mEdit->setPlaceholderText(QString::fromUtf8(_("Paste a public key export here. It should look like:")) +
QStringLiteral("\n\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n"
"mQENBEzJavsBCADG/guWL6AxGgngUxp/DcmoitJjaJMqcJkBtD3uKrW81Pbnm3LI\n"
"...\n"
"dCl8hHggB9x2\n"
"=oShe\n"
"-----END PGP PUBLIC KEY BLOCK-----"));
mEdit->setUndoRedoEnabled(true);
+
+ mCMSEdit->setFont(fixedFont);
+ mCMSEdit->setPlaceholderText(QString::fromUtf8(_("Paste the certificate here. It should look like:")) +
+ QStringLiteral("\n\n-----BEGIN CERTIFICATE-----\n\n"
+ "MIICeDCCAeGgAwIBAgIJANNFIDoYY4XJMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV\n"
+ "...\n"
+ "dCl8hHggB9x2\n"
+ "-----END CERTIFICATE-----"));
+ mCMSEdit->setUndoRedoEnabled(true);
+
/* Setup buttons */
QDialogButtonBox *buttonBox = new QDialogButtonBox();
buttonBox->setStandardButtons(QDialogButtonBox::Cancel |
QDialogButtonBox::Ok);
connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked,
this, [this] () {
checkAccept();
});
connect(buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked,
this, [this] () {
qApp->quit();
});
auto okBtn = buttonBox->button(QDialogButtonBox::Ok);
okBtn->setEnabled(false);
connect(mEdit, &QTextEdit::textChanged, this, [this, okBtn] () {
const auto text = mEdit->toPlainText().trimmed().toUtf8();
if (!text.size()) {
okBtn->setEnabled(true);
return;
}
GpgME::Data data(text.constData(), text.size(), false);
okBtn->setEnabled(data.type() == GpgME::Data::PGPKey);
});
+ connect(mCMSEdit, &QTextEdit::textChanged, this, [this, okBtn] () {
+ const auto text = mCMSEdit->toPlainText().trimmed().toUtf8();
+ if (!text.size()) {
+ okBtn->setEnabled(true);
+ return;
+ }
+ GpgME::Data data(text.constData(), text.size(), false);
+
+ okBtn->setEnabled(data.type() == GpgME::Data::X509Cert);
+ });
+
+ /* The always secure box */
+ mAlwaysSec->setText(QString::fromUtf8(_("Always secure mails")));
/* Setup layout */
auto layout = new QVBoxLayout;
setLayout(layout);
- layout->addWidget(mEdit);
+
+ layout->addWidget(mAlwaysSec);
+
+ auto hBox = new QHBoxLayout;
+
+ hBox->addWidget(new QLabel(
+ _("Use specific keys / certs for this contact:")));
+
+ auto infoBtn = new QPushButton;
+ infoBtn->setIcon(QIcon::fromTheme("help-contextual"));
+ infoBtn->setFlat(true);
+ hBox->addWidget(infoBtn);
+ hBox->addStretch(1);
+
+ connect(infoBtn, &QPushButton::clicked, this, [this, infoBtn] () {
+ const QString generalMsg = QString::fromUtf8(_("You can use this to override the keys / certificates used "
+ "for this contact. The keys will be imported and used for "
+ "this contact, regardless of OpenPGP trust."));
+ const QString smimeMsg = QString::fromUtf8(_("For S/MIME the root certificate has to be trusted."));
+ const QString multiMsg = QString::fromUtf8(_("Place multiple keys / certificates in here to encrypt to all of them."));
+ QToolTip::showText(infoBtn->mapToGlobal(QPoint()),
+ QStringLiteral("%1<br/><br/>%2").arg(generalMsg).arg(
+ (mShowCMS ? smimeMsg + QStringLiteral("<br/><br/>") + multiMsg :
+ multiMsg)), infoBtn, QRect(), 30000);
+ });
+
+
+ layout->addLayout(hBox);
+
+ if (mShowCMS) {
+ auto tab = new QTabWidget;
+ tab->addTab(mEdit, QStringLiteral("OpenPGP"));
+ tab->addTab(mCMSEdit, QStringLiteral("S/MIME (X509)"));
+ layout->addWidget(tab);
+ } else {
+ layout->addWidget(mEdit);
+ }
layout->addWidget(buttonBox);
}
-static void save(const QByteArray &data)
+void GpgOLKeyAdder::save()
{
- if (!data.size()) {
+ std::cout << "BEGIN KEYADDER PGP DATA" << std::endl;
+ const QByteArray &pgpData = mEdit->toPlainText().trimmed().toUtf8();
+ if (!pgpData.size()) {
+ /* Empty is a special case which can mean that an
+ * existing key should be removed. */
+ std::cout << "empty" << std::endl;
+ } else {
+ std::cout << pgpData.constData() << std::endl;
+ }
+ std::cout << "END KEYADDER PGP DATA" << std::endl;
+
+ std::cout << "BEGIN KEYADDER CMS DATA" << std::endl;
+ const QByteArray &cmsData = mCMSEdit->toPlainText().trimmed().toUtf8();
+ if (!cmsData.size()) {
/* Empty is a special case which can mean that an
* existing key should be removed. */
std::cout << "empty" << std::endl;
} else {
- std::cout << data.constData() << std::endl;
+ std::cout << cmsData.constData() << std::endl;
}
+ std::cout << "END KEYADDER CMS DATA" << std::endl;
+ std::cout << "BEGIN KEYADDER OPTIONS" << std::endl;
+ std::cout << "secure=" << (mAlwaysSec->isChecked() ? "3" : "0") << std::endl;
+ std::cout << "END KEYADDER OPTIONS" << std::endl;
+
qApp->quit();
}
static QString prettyNameAndEMail(const QString &id, const QString &name, const QString &email, const QString &comment)
{
if (name.isEmpty()) {
if (email.isEmpty()) {
return QString();
} else if (comment.isEmpty()) {
return QStringLiteral("<%1>").arg(email);
} else {
return QStringLiteral("(%2) <%1>").arg(email, comment);
}
}
if (email.isEmpty()) {
if (comment.isEmpty()) {
return name;
} else {
return QStringLiteral("%1 (%2)").arg(name, comment);
}
}
if (comment.isEmpty()) {
return QStringLiteral("%1 <%2>").arg(name, email);
} else {
return QStringLiteral("%1 (%3) <%2>").arg(name, email, comment);
}
return QString();
}
static QString prettyNameAndEMail(const char *id, const char *name_, const std::string &email_, const char *comment_)
{
return prettyNameAndEMail(QString::fromUtf8(id), QString::fromUtf8(name_), QString::fromStdString(email_),
QString::fromUtf8(comment_));
}
static QString prettyNameAndEMail(const GpgME::UserID &uid)
{
return prettyNameAndEMail(uid.id(), uid.name(), uid.addrSpec(), uid.comment());
}
static QString time_t2string(time_t t)
{
QDateTime dt;
dt.setTime_t(t);
return QLocale().toString(dt, QLocale::ShortFormat);
}
-void GpgOLKeyAdder::checkAccept()
+static QStringList
+buildKeyInfos(const std::vector<GpgME::Key> keys, QWidget *parent,
+ bool *bOk)
{
- const auto text = mEdit->toPlainText().trimmed().toUtf8();
-
- if (text.isEmpty()) {
- save(text);
- // Save exits
- return;
- }
-
- GpgME::Data data(text.constData(), text.size(), false);
-
- const auto keys = data.toKeys();
+ QStringList keyInfos;
- if (!keys.size()) {
- QMessageBox::warning(this, QString::fromUtf8 (_("Error")),
- QStringLiteral("<b>%1</b><br/><br/>").arg(QString::fromUtf8(_("Failed to parse any public key."))));
- return;
+ if (!bOk) {
+ qDebug() << "Invalid call";
+ return keyInfos;
}
-
- QStringList keyInfos;
+ *bOk = false;
for (const auto &key: keys) {
if (key.isNull() || !key.numSubkeys()) {
qDebug() << "Null key?";
continue;
}
-
- if (key.hasSecret()) {
- QMessageBox::warning(this, QString::fromUtf8 (_("Error")),
+ if (key.hasSecret() && key.protocol() == GpgME::OpenPGP) {
+ QMessageBox::warning(parent, QString::fromUtf8 (_("Error")),
QStringLiteral("<b>%1</b><br/><br/>").arg(QString::fromUtf8(_("Secret key detected."))) +
QString::fromUtf8(_("You can only configure public keys in Outlook."
" Import secret keys with Kleopatra.")));
- return;
+ return QStringList();
}
- if (key.isRevoked() || key.isExpired() || key.isInvalid() || key.isDisabled() || !key.canEncrypt()) {
- QMessageBox::warning(this, QString::fromUtf8 (_("Error")),
+ if (key.isBad() || !key.canEncrypt()) {
+ QMessageBox::warning(parent, QString::fromUtf8 (_("Error")),
QStringLiteral("<b>%1</b><br/><br/>%2<br/><br/>").arg(QString::fromUtf8(_("Invalid key detected."))).arg(
key.primaryFingerprint()) +
QString::fromUtf8(_("The key is unusable for Outlook."
" Please check Kleopatra for more information.")));
- return;
+ return QStringList();
}
const auto subkey = key.subkey(0);
QString info = QString::fromLatin1(key.primaryFingerprint()) + "<br/>" +
QString::fromUtf8(_("Created:")) + " " + time_t2string(subkey.creationTime()) + "<br/>" +
QString::fromUtf8(_("User Ids:")) + "<br/>";
for (const auto &uid: key.userIDs()) {
if (uid.isNull() || uid.isRevoked() || uid.isInvalid()) {
continue;
}
info += "&nbsp;&nbsp;" + prettyNameAndEMail(uid).toHtmlEscaped() + "<br/>";
}
keyInfos << info;
}
+ *bOk = true;
+ return keyInfos;
+}
+
+void GpgOLKeyAdder::checkAcceptBottom(const std::vector<GpgME::Key> &pgpKeys, const std::vector<GpgME::Key> &cmsCerts)
+{
+ bool ok;
+ QString msg;
+
+ if (!pgpKeys.size() && (mShowCMS && !pgpKeys.size() && !cmsCerts.size())) {
+ QMessageBox::warning(this, QString::fromUtf8 (_("Error")),
+ QStringLiteral("<b>%1</b><br/><br/>").arg(QString::fromUtf8(_("Failed to parse any public key."))));
+ return;
+ }
- QString msg = QString::fromUtf8(_("You are about to configure the following %1 for:")).arg(
- (keyInfos.size() > 1 ? QString::fromUtf8(_("keys")) : QString::fromUtf8(_("key")))) +
+ if (pgpKeys.size())
+ {
+ const auto keyInfos = buildKeyInfos (pgpKeys, this, &ok);
+ if (!ok) {
+ return;
+ }
+ msg += QString::fromUtf8(_("You are about to configure the following OpenPGP %1 for:")).arg(
+ (keyInfos.size() > 1 ? QString::fromUtf8(_("keys")) : QString::fromUtf8(_("key")))) +
QStringLiteral("<br/>\t\"%1\"<br/><br/>").arg(mName.toHtmlEscaped()) +
- keyInfos.join("<br/><br/>");
+ keyInfos.join("<br/><br/>");
+
+ }
+ if (mShowCMS && cmsCerts.size())
+ {
+ std::vector<GpgME::Key> leafs;
+ std::remove_copy_if(cmsCerts.begin(), cmsCerts.end(),
+ std::back_inserter(leafs),
+ [cmsCerts] (const auto &k) {
+ /* Check if a key has this fingerprint in the
+ * chain ID. Meaning that there is any child of
+ * this certificate. In that case remove it. */
+ for (const auto &c: cmsCerts) {
+ if (!c.chainID()) {
+ continue;
+ }
+ if (!k.primaryFingerprint() || !c.primaryFingerprint()) {
+ /* WTF? */
+ continue;
+ }
+ if (!strcmp (c.chainID(), k.primaryFingerprint())) {
+ qDebug() << "Filtering out non leaf cert" << k.primaryFingerprint();
+ return true;
+ }
+ }
+ return false;
+ });
+
+ const auto certInfos = buildKeyInfos (leafs, this, &ok);
+ if (!ok) {
+ return;
+ }
+ msg += QString::fromUtf8(_("You are about to configure the following S/MIME %1 for:")).arg(
+ (certInfos.size() > 1 ? QString::fromUtf8(_("certificates")) : QString::fromUtf8(_("certificate")))) +
+ QStringLiteral("<br/>\t\"%1\"<br/><br/>").arg(mName.toHtmlEscaped()) +
+ certInfos.join("<br/><br/>");
+ }
msg += "<br/><br/>" + QString::fromUtf8 (_("Continue?"));
- const auto ret = QMessageBox::question(this, QString::fromUtf8(_("Confirm keys")), msg,
+ const auto ret = QMessageBox::question(this, QString::fromUtf8(_("Confirm")), msg,
QMessageBox::Yes | QMessageBox::Abort, QMessageBox::Yes);
if (ret == QMessageBox::Yes) {
- save(text);
+ save();
return;
}
}
+
+void GpgOLKeyAdder::checkAccept()
+{
+ const auto openpgpText = mEdit->toPlainText().trimmed().toUtf8();
+ const auto cmsText = mCMSEdit->toPlainText().trimmed().toUtf8();
+
+ GpgME::Data pgp(openpgpText.constData(), openpgpText.size(), false);
+ GpgME::Data cms(cmsText.constData(), cmsText.size(), false);
+
+ const auto keys = pgp.toKeys(GpgME::OpenPGP);
+
+ if (!mShowCMS || !cmsText.size()) {
+ checkAcceptBottom (keys, std::vector<GpgME::Key>());
+ return;
+ }
+
+ auto progress = new QProgressDialog(this, Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::Dialog);
+ progress->setAutoClose(true);
+ progress->setMinimumDuration(0);
+ progress->setMaximum(0);
+ progress->setMinimum(0);
+ progress->setModal(true);
+ progress->setCancelButton(nullptr);
+ progress->setWindowTitle(_("Validating S/MIME certificates"));
+ progress->setLabel(new QLabel(_("This may take several minutes...")));
+ auto workerThread = new CMSImportThread(cms);
+ connect(workerThread, &QThread::finished, this, [this, workerThread, progress, keys] {
+ progress->accept();
+ progress->deleteLater();
+ checkAcceptBottom(keys, workerThread->certs());
+ delete workerThread;
+ });
+ workerThread->start();
+ progress->exec();
+}
+
+#include "gpgolkeyadder.moc"
diff --git a/src/gpgolkeyadder/gpgolkeyadder.h b/src/gpgolkeyadder/gpgolkeyadder.h
index 22c9d80..7813653 100644
--- a/src/gpgolkeyadder/gpgolkeyadder.h
+++ b/src/gpgolkeyadder/gpgolkeyadder.h
@@ -1,33 +1,44 @@
#ifndef GPGOLKEYADDER_H
#define GPGOLKEYADDER_H
/* Copyright (C) 2018 by Intevation GmbH <info@intevation.de>
*
* This file is Free Software under the GNU GPL (v>=2)
* and comes with ABSOLUTELY NO WARRANTY!
* See LICENSE.txt for details.
*/
#include <QDialog>
#include <QString>
+#include <vector>
+#include <gpgme++/key.h>
+
class QCommandLineParser;
class QTextEdit;
+class QCheckBox;
+
class GpgOLKeyAdder: public QDialog
{
Q_OBJECT
public:
GpgOLKeyAdder(const QCommandLineParser &parser);
protected:
/** @brief UI setup */
void setupGUI();
private:
void checkAccept();
+ void checkAcceptBottom(const std::vector<GpgME::Key> &pgpKeys, const std::vector<GpgME::Key> &cmsKeys);
+ void save();
+ void handleInput(const QByteArray &data);
QTextEdit *mEdit;
+ QTextEdit *mCMSEdit;
QString mName;
+ QCheckBox *mAlwaysSec;
+ bool mShowCMS;
};
#endif // GPGOLKEYADDER_H

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 8, 11:36 AM (1 d, 13 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
a0/a7/b6b37d842391a91d5485c217a379

Event Timeline