File indexing completed on 2024-06-02 05:24:12
0001 /* -*- mode: c++; c-basic-offset:4 -*- 0002 exportcertificatecommand.cpp 0003 0004 This file is part of Kleopatra, the KDE keymanager 0005 SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB 0006 SPDX-FileCopyrightText: 2021 g10 Code GmbH 0007 0008 SPDX-License-Identifier: GPL-2.0-or-later 0009 */ 0010 0011 #include <config-kleopatra.h> 0012 0013 #include "exportcertificatecommand.h" 0014 #include "fileoperationspreferences.h" 0015 0016 #include "command_p.h" 0017 0018 #include <utils/applicationstate.h> 0019 #include <utils/filedialog.h> 0020 0021 #include <Libkleo/Algorithm> 0022 #include <Libkleo/Classify> 0023 #include <Libkleo/Formatting> 0024 #include <Libkleo/KeyHelpers> 0025 0026 #include <QGpgME/ExportJob> 0027 #include <QGpgME/Protocol> 0028 0029 #include <gpgme++/key.h> 0030 0031 #include <KLocalizedString> 0032 #include <QSaveFile> 0033 0034 #include <QFileInfo> 0035 #include <QMap> 0036 #include <QPointer> 0037 0038 #include <algorithm> 0039 #include <vector> 0040 0041 using namespace Kleo; 0042 using namespace GpgME; 0043 using namespace QGpgME; 0044 0045 class ExportCertificateCommand::Private : public Command::Private 0046 { 0047 friend class ::ExportCertificateCommand; 0048 ExportCertificateCommand *q_func() const 0049 { 0050 return static_cast<ExportCertificateCommand *>(q); 0051 } 0052 0053 public: 0054 explicit Private(ExportCertificateCommand *qq, KeyListController *c); 0055 ~Private() override; 0056 void startExportJob(GpgME::Protocol protocol, const std::vector<Key> &keys); 0057 void cancelJobs(); 0058 void exportResult(const GpgME::Error &, const QByteArray &); 0059 void showError(const GpgME::Error &error); 0060 0061 bool confirmExport(const std::vector<Key> &pgpKeys); 0062 bool requestFileNames(GpgME::Protocol prot); 0063 void finishedIfLastJob(); 0064 0065 private: 0066 QMap<GpgME::Protocol, QString> fileNames; 0067 uint jobsPending = 0; 0068 QMap<QObject *, QString> outFileForSender; 0069 QPointer<ExportJob> cmsJob; 0070 QPointer<ExportJob> pgpJob; 0071 }; 0072 0073 ExportCertificateCommand::Private *ExportCertificateCommand::d_func() 0074 { 0075 return static_cast<Private *>(d.get()); 0076 } 0077 const ExportCertificateCommand::Private *ExportCertificateCommand::d_func() const 0078 { 0079 return static_cast<const Private *>(d.get()); 0080 } 0081 0082 #define d d_func() 0083 #define q q_func() 0084 0085 ExportCertificateCommand::Private::Private(ExportCertificateCommand *qq, KeyListController *c) 0086 : Command::Private(qq, c) 0087 { 0088 } 0089 0090 ExportCertificateCommand::Private::~Private() 0091 { 0092 } 0093 0094 ExportCertificateCommand::ExportCertificateCommand(KeyListController *p) 0095 : Command(new Private(this, p)) 0096 { 0097 } 0098 0099 ExportCertificateCommand::ExportCertificateCommand(QAbstractItemView *v, KeyListController *p) 0100 : Command(v, new Private(this, p)) 0101 { 0102 } 0103 0104 ExportCertificateCommand::ExportCertificateCommand(const Key &key) 0105 : Command(key, new Private(this, nullptr)) 0106 { 0107 } 0108 0109 ExportCertificateCommand::~ExportCertificateCommand() 0110 { 0111 } 0112 0113 void ExportCertificateCommand::setOpenPGPFileName(const QString &fileName) 0114 { 0115 if (!d->jobsPending) { 0116 d->fileNames[OpenPGP] = fileName; 0117 } 0118 } 0119 0120 QString ExportCertificateCommand::openPGPFileName() const 0121 { 0122 return d->fileNames[OpenPGP]; 0123 } 0124 0125 void ExportCertificateCommand::setX509FileName(const QString &fileName) 0126 { 0127 if (!d->jobsPending) { 0128 d->fileNames[CMS] = fileName; 0129 } 0130 } 0131 0132 QString ExportCertificateCommand::x509FileName() const 0133 { 0134 return d->fileNames[CMS]; 0135 } 0136 0137 void ExportCertificateCommand::doStart() 0138 { 0139 if (d->keys().empty()) { 0140 d->finished(); 0141 return; 0142 } 0143 0144 const auto keys = Kleo::partitionKeysByProtocol(d->keys()); 0145 0146 if (!keys.openpgp.empty() && !d->confirmExport(keys.openpgp)) { 0147 d->canceled(); 0148 return; 0149 } 0150 0151 const bool haveBoth = !keys.cms.empty() && !keys.openpgp.empty(); 0152 const GpgME::Protocol prot = haveBoth ? UnknownProtocol : (!keys.cms.empty() ? CMS : OpenPGP); 0153 if (!d->requestFileNames(prot)) { 0154 d->canceled(); 0155 return; 0156 } 0157 0158 if (!keys.openpgp.empty()) { 0159 d->startExportJob(GpgME::OpenPGP, keys.openpgp); 0160 } 0161 if (!keys.cms.empty()) { 0162 d->startExportJob(GpgME::CMS, keys.cms); 0163 } 0164 } 0165 0166 bool ExportCertificateCommand::Private::confirmExport(const std::vector<Key> &pgpKeys) 0167 { 0168 auto notCertifiedKeys = std::accumulate(pgpKeys.cbegin(), pgpKeys.cend(), QStringList{}, [](auto keyNames, const auto &key) { 0169 const bool allValidUserIDsAreCertifiedByUser = Kleo::all_of(key.userIDs(), [](const UserID &userId) { 0170 return userId.isBad() || Kleo::userIDIsCertifiedByUser(userId); 0171 }); 0172 if (!allValidUserIDsAreCertifiedByUser) { 0173 keyNames.push_back(Formatting::formatForComboBox(key)); 0174 } 0175 return keyNames; 0176 }); 0177 if (!notCertifiedKeys.empty()) { 0178 if (pgpKeys.size() == 1) { 0179 const auto answer = KMessageBox::warningContinueCancel( // 0180 parentWidgetOrView(), 0181 xi18nc("@info", 0182 "<para>You haven't certified all valid user IDs of this certificate " 0183 "with an exportable certification. People relying on your certifications " 0184 "may not be able to verify the certificate.</para>" 0185 "<para>Do you want to continue the export?</para>"), 0186 i18nc("@title:window", "Confirm Certificate Export"), 0187 KGuiItem{i18ncp("@action:button", "Export Certificate", "Export Certificates", 1)}, 0188 KStandardGuiItem::cancel(), 0189 QStringLiteral("confirm-export-of-uncertified-keys")); 0190 return answer == KMessageBox::Continue; 0191 } else { 0192 std::sort(notCertifiedKeys.begin(), notCertifiedKeys.end()); 0193 const auto answer = KMessageBox::warningContinueCancelList( // 0194 parentWidgetOrView(), 0195 xi18nc("@info", 0196 "<para>You haven't certified all valid user IDs of the certificates listed below " 0197 "with exportable certifications. People relying on your certifications " 0198 "may not be able to verify the certificates.</para>" 0199 "<para>Do you want to continue the export?</para>"), 0200 notCertifiedKeys, 0201 i18nc("@title:window", "Confirm Certificate Export"), 0202 KGuiItem{i18ncp("@action:button", "Export Certificate", "Export Certificates", pgpKeys.size())}, 0203 KStandardGuiItem::cancel(), 0204 QStringLiteral("confirm-export-of-uncertified-keys")); 0205 return answer == KMessageBox::Continue; 0206 } 0207 } 0208 0209 return true; 0210 } 0211 0212 bool ExportCertificateCommand::Private::requestFileNames(GpgME::Protocol protocol) 0213 { 0214 if (protocol == UnknownProtocol) { 0215 if (!fileNames[GpgME::OpenPGP].isEmpty() && !fileNames[GpgME::CMS].isEmpty()) { 0216 return true; 0217 } 0218 0219 /* Unknown protocol ask for first PGP Export file name */ 0220 if (fileNames[GpgME::OpenPGP].isEmpty() && !requestFileNames(GpgME::OpenPGP)) { 0221 return false; 0222 } 0223 /* And then for CMS */ 0224 return requestFileNames(GpgME::CMS); 0225 } 0226 0227 if (!fileNames[protocol].isEmpty()) { 0228 return true; 0229 } 0230 0231 const auto lastDir = ApplicationState::lastUsedExportDirectory(); 0232 0233 QString proposedFileName = lastDir + QLatin1Char('/'); 0234 if (keys().size() == 1) { 0235 const bool usePGPFileExt = FileOperationsPreferences().usePGPFileExt(); 0236 const auto key = keys().front(); 0237 auto name = Formatting::prettyName(key); 0238 if (name.isEmpty()) { 0239 name = Formatting::prettyEMail(key); 0240 } 0241 const auto asciiArmoredCertificateClass = (protocol == OpenPGP ? Class::OpenPGP : Class::CMS) | Class::Ascii | Class::Certificate; 0242 /* Not translated so it's better to use in tutorials etc. */ 0243 proposedFileName += QStringLiteral("%1_%2_public.%3") 0244 .arg(name) 0245 .arg(Formatting::prettyKeyID(key.shortKeyID())) 0246 .arg(outputFileExtension(asciiArmoredCertificateClass, usePGPFileExt)); 0247 } 0248 if (protocol == GpgME::CMS) { 0249 if (!fileNames[GpgME::OpenPGP].isEmpty()) { 0250 /* If the user has already selected a PGP file name then use that as basis 0251 * for a proposal for the S/MIME file. */ 0252 proposedFileName = fileNames[GpgME::OpenPGP]; 0253 const int idx = proposedFileName.size() - 4; 0254 if (proposedFileName.endsWith(QLatin1StringView(".asc"))) { 0255 proposedFileName.replace(idx, 4, QLatin1StringView(".pem")); 0256 } 0257 if (proposedFileName.endsWith(QLatin1StringView(".gpg")) || proposedFileName.endsWith(QLatin1String(".pgp"))) { 0258 proposedFileName.replace(idx, 4, QLatin1StringView(".der")); 0259 } 0260 } 0261 } 0262 0263 if (proposedFileName.isEmpty()) { 0264 proposedFileName = lastDir; 0265 proposedFileName += i18nc("A generic filename for exported certificates", "certificates"); 0266 proposedFileName += protocol == GpgME::OpenPGP ? QStringLiteral(".asc") : QStringLiteral(".pem"); 0267 } 0268 0269 auto fname = FileDialog::getSaveFileNameEx(parentWidgetOrView(), 0270 i18nc("1 is protocol", "Export %1 Certificates", Formatting::displayName(protocol)), 0271 QStringLiteral("imp"), 0272 proposedFileName, 0273 protocol == GpgME::OpenPGP ? i18n("OpenPGP Certificates") + QLatin1StringView(" (*.asc *.gpg *.pgp)") 0274 : i18n("S/MIME Certificates") + QLatin1StringView(" (*.pem *.der)")); 0275 0276 if (!fname.isEmpty() && protocol == GpgME::CMS && fileNames[GpgME::OpenPGP] == fname) { 0277 KMessageBox::error(parentWidgetOrView(), 0278 i18n("You have to select different filenames for different protocols."), 0279 i18nc("@title:window", "Export Error")); 0280 return false; 0281 } 0282 const QFileInfo fi(fname); 0283 if (fi.suffix().isEmpty()) { 0284 fname += protocol == GpgME::OpenPGP ? QStringLiteral(".asc") : QStringLiteral(".pem"); 0285 } 0286 0287 fileNames[protocol] = fname; 0288 ApplicationState::setLastUsedExportDirectory(fi.absolutePath()); 0289 return !fname.isEmpty(); 0290 } 0291 0292 void ExportCertificateCommand::Private::startExportJob(GpgME::Protocol protocol, const std::vector<Key> &keys) 0293 { 0294 Q_ASSERT(protocol != GpgME::UnknownProtocol); 0295 0296 const QGpgME::Protocol *const backend = (protocol == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); 0297 Q_ASSERT(backend); 0298 const QString fileName = fileNames[protocol]; 0299 const bool binary = protocol == GpgME::OpenPGP 0300 ? fileName.endsWith(QLatin1StringView(".gpg"), Qt::CaseInsensitive) || fileName.endsWith(QLatin1String(".pgp"), Qt::CaseInsensitive) 0301 : fileName.endsWith(QLatin1StringView(".der"), Qt::CaseInsensitive); 0302 std::unique_ptr<ExportJob> job(backend->publicKeyExportJob(!binary)); 0303 Q_ASSERT(job.get()); 0304 0305 connect(job.get(), &QGpgME::ExportJob::result, q, [this](const GpgME::Error &result, const QByteArray &keyData) { 0306 exportResult(result, keyData); 0307 }); 0308 0309 connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress); 0310 0311 QStringList fingerprints; 0312 fingerprints.reserve(keys.size()); 0313 for (const Key &i : keys) { 0314 fingerprints << QLatin1StringView(i.primaryFingerprint()); 0315 } 0316 0317 const GpgME::Error err = job->start(fingerprints); 0318 if (err) { 0319 showError(err); 0320 finished(); 0321 return; 0322 } 0323 Q_EMIT q->info(i18n("Exporting certificates...")); 0324 ++jobsPending; 0325 const QPointer<ExportJob> exportJob(job.release()); 0326 0327 outFileForSender[exportJob.data()] = fileName; 0328 (protocol == CMS ? cmsJob : pgpJob) = exportJob; 0329 } 0330 0331 void ExportCertificateCommand::Private::showError(const GpgME::Error &err) 0332 { 0333 Q_ASSERT(err); 0334 const QString msg = i18n( 0335 "<qt><p>An error occurred while trying to export " 0336 "the certificate:</p>" 0337 "<p><b>%1</b></p></qt>", 0338 Formatting::errorAsString(err)); 0339 error(msg, i18n("Certificate Export Failed")); 0340 } 0341 0342 void ExportCertificateCommand::doCancel() 0343 { 0344 d->cancelJobs(); 0345 } 0346 0347 void ExportCertificateCommand::Private::finishedIfLastJob() 0348 { 0349 if (jobsPending <= 0) { 0350 finished(); 0351 } 0352 } 0353 0354 static bool write_complete(QIODevice &iod, const QByteArray &data) 0355 { 0356 qint64 total = 0; 0357 qint64 toWrite = data.size(); 0358 while (total < toWrite) { 0359 const qint64 written = iod.write(data.data() + total, toWrite); 0360 if (written < 0) { 0361 return false; 0362 } 0363 total += written; 0364 toWrite -= written; 0365 } 0366 return true; 0367 } 0368 0369 void ExportCertificateCommand::Private::exportResult(const GpgME::Error &err, const QByteArray &data) 0370 { 0371 Q_ASSERT(jobsPending > 0); 0372 --jobsPending; 0373 0374 Q_ASSERT(outFileForSender.contains(q->sender())); 0375 const QString outFile = outFileForSender[q->sender()]; 0376 0377 if (err) { 0378 showError(err); 0379 finishedIfLastJob(); 0380 return; 0381 } 0382 QSaveFile savefile(outFile); 0383 // TODO: use KIO 0384 const QString writeErrorMsg = i18n("Could not write to file %1.", outFile); 0385 const QString errorCaption = i18n("Certificate Export Failed"); 0386 if (!savefile.open(QIODevice::WriteOnly)) { 0387 error(writeErrorMsg, errorCaption); 0388 finishedIfLastJob(); 0389 return; 0390 } 0391 0392 if (!write_complete(savefile, data) || !savefile.commit()) { 0393 error(writeErrorMsg, errorCaption); 0394 } 0395 finishedIfLastJob(); 0396 } 0397 0398 void ExportCertificateCommand::Private::cancelJobs() 0399 { 0400 if (cmsJob) { 0401 cmsJob->slotCancel(); 0402 } 0403 if (pgpJob) { 0404 pgpJob->slotCancel(); 0405 } 0406 } 0407 0408 #undef d 0409 #undef q 0410 0411 #include "moc_exportcertificatecommand.cpp"