1 /*
2   SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
3   SPDX-FileCopyrightText: 2009 Leo Franchi <lfranchi@kde.org>
4 
5   SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "job/signjob.h"
9 
10 #include "contentjobbase_p.h"
11 #include "job/protectedheadersjob.h"
12 #include "utils/util_p.h"
13 
14 #include <QGpgME/Protocol>
15 #include <QGpgME/SignJob>
16 #include <QVector>
17 
18 #include "messagecomposer_debug.h"
19 #include <KMime/Content>
20 #include <KMime/Headers>
21 #include <KMime/KMimeMessage>
22 
23 #include <gpgme++/encryptionresult.h>
24 #include <gpgme++/global.h>
25 #include <gpgme++/signingresult.h>
26 #include <sstream>
27 
28 using namespace MessageComposer;
29 
30 class MessageComposer::SignJobPrivate : public ContentJobBasePrivate
31 {
32 public:
SignJobPrivate(SignJob * qq)33     SignJobPrivate(SignJob *qq)
34         : ContentJobBasePrivate(qq)
35     {
36     }
37 
38     KMime::Content *content = nullptr;
39     KMime::Message *skeletonMessage = nullptr;
40     std::vector<GpgME::Key> signers;
41     Kleo::CryptoMessageFormat format;
42 
43     bool protectedHeaders = true;
44 
45     // copied from messagecomposer.cpp
binaryHint(Kleo::CryptoMessageFormat f)46     Q_REQUIRED_RESULT bool binaryHint(Kleo::CryptoMessageFormat f)
47     {
48         switch (f) {
49         case Kleo::SMIMEFormat:
50         case Kleo::SMIMEOpaqueFormat:
51             return true;
52         default:
53         case Kleo::OpenPGPMIMEFormat:
54         case Kleo::InlineOpenPGPFormat:
55             return false;
56         }
57     }
58 
signingMode(Kleo::CryptoMessageFormat f)59     Q_REQUIRED_RESULT GpgME::SignatureMode signingMode(Kleo::CryptoMessageFormat f)
60     {
61         switch (f) {
62         case Kleo::SMIMEOpaqueFormat:
63             return GpgME::NormalSignatureMode;
64         case Kleo::InlineOpenPGPFormat:
65             return GpgME::Clearsigned;
66         default:
67         case Kleo::SMIMEFormat:
68         case Kleo::OpenPGPMIMEFormat:
69             return GpgME::Detached;
70         }
71     }
72 
73     Q_DECLARE_PUBLIC(SignJob)
74 };
75 
SignJob(QObject * parent)76 SignJob::SignJob(QObject *parent)
77     : ContentJobBase(*new SignJobPrivate(this), parent)
78 {
79 }
80 
~SignJob()81 SignJob::~SignJob()
82 {
83 }
84 
setContent(KMime::Content * content)85 void SignJob::setContent(KMime::Content *content)
86 {
87     Q_D(SignJob);
88 
89     d->content = content;
90 }
91 
setCryptoMessageFormat(Kleo::CryptoMessageFormat format)92 void SignJob::setCryptoMessageFormat(Kleo::CryptoMessageFormat format)
93 {
94     Q_D(SignJob);
95 
96     // There *must* be a concrete format set at this point.
97     Q_ASSERT(format == Kleo::OpenPGPMIMEFormat || format == Kleo::InlineOpenPGPFormat || format == Kleo::SMIMEFormat || format == Kleo::SMIMEOpaqueFormat);
98     d->format = format;
99 }
100 
setSigningKeys(const std::vector<GpgME::Key> & signers)101 void SignJob::setSigningKeys(const std::vector<GpgME::Key> &signers)
102 {
103     Q_D(SignJob);
104 
105     d->signers = signers;
106 }
107 
setSkeletonMessage(KMime::Message * skeletonMessage)108 void SignJob::setSkeletonMessage(KMime::Message *skeletonMessage)
109 {
110     Q_D(SignJob);
111 
112     d->skeletonMessage = skeletonMessage;
113 }
114 
setProtectedHeaders(bool protectedHeaders)115 void SignJob::setProtectedHeaders(bool protectedHeaders)
116 {
117     Q_D(SignJob);
118 
119     d->protectedHeaders = protectedHeaders;
120 }
121 
origContent()122 KMime::Content *SignJob::origContent()
123 {
124     Q_D(SignJob);
125 
126     return d->content;
127 }
128 
doStart()129 void SignJob::doStart()
130 {
131     Q_D(SignJob);
132     Q_ASSERT(d->resultContent == nullptr); // Not processed before.
133 
134     if (d->protectedHeaders && d->skeletonMessage && d->format & Kleo::OpenPGPMIMEFormat) {
135         auto pJob = new ProtectedHeadersJob;
136         pJob->setContent(d->content);
137         pJob->setSkeletonMessage(d->skeletonMessage);
138         pJob->setObvoscate(false);
139         QObject::connect(pJob, &ProtectedHeadersJob::finished, this, [d, pJob](KJob *job) {
140             if (job->error()) {
141                 return;
142             }
143             d->content = pJob->content();
144         });
145         appendSubjob(pJob);
146     }
147 
148     ContentJobBase::doStart();
149 }
150 
slotResult(KJob * job)151 void SignJob::slotResult(KJob *job)
152 {
153     if (error() || job->error()) {
154         ContentJobBase::slotResult(job);
155         return;
156     }
157     if (subjobs().size() == 2) {
158         auto pjob = static_cast<ProtectedHeadersJob *>(subjobs().last());
159         if (pjob) {
160             auto cjob = qobject_cast<ContentJobBase *>(job);
161             Q_ASSERT(cjob);
162             pjob->setContent(cjob->content());
163         }
164     }
165 
166     ContentJobBase::slotResult(job);
167 }
168 
process()169 void SignJob::process()
170 {
171     Q_D(SignJob);
172     Q_ASSERT(d->resultContent == nullptr); // Not processed before.
173 
174     // if setContent hasn't been called, we assume that a subjob was added
175     // and we want to use that
176     if (!d->content) {
177         Q_ASSERT(d->subjobContents.size() == 1);
178         d->content = d->subjobContents.constFirst();
179     }
180 
181     // d->resultContent = new KMime::Content;
182 
183     const QGpgME::Protocol *proto = nullptr;
184     if (d->format & Kleo::AnyOpenPGP) {
185         proto = QGpgME::openpgp();
186     } else if (d->format & Kleo::AnySMIME) {
187         proto = QGpgME::smime();
188     }
189 
190     Q_ASSERT(proto);
191 
192     qCDebug(MESSAGECOMPOSER_LOG) << "creating signJob from:" << proto->name() << proto->displayName();
193     // for now just do the main recipients
194     QByteArray signature;
195 
196     d->content->assemble();
197 
198     // replace simple LFs by CRLFs for all MIME supporting CryptPlugs
199     // according to RfC 2633, 3.1.1 Canonicalization
200     QByteArray content;
201     if (d->format & Kleo::InlineOpenPGPFormat) {
202         content = d->content->body();
203     } else if (!(d->format & Kleo::SMIMEOpaqueFormat)) {
204         // replace "From " and "--" at the beginning of lines
205         // with encoded versions according to RfC 3156, 3
206         // Note: If any line begins with the string "From ", it is strongly
207         //   suggested that either the Quoted-Printable or Base64 MIME encoding
208         //   be applied.
209         const auto encoding = d->content->contentTransferEncoding()->encoding();
210         if ((encoding == KMime::Headers::CEquPr || encoding == KMime::Headers::CE7Bit) && !d->content->contentType(false)) {
211             QByteArray body = d->content->encodedBody();
212             bool changed = false;
213             QVector<QByteArray> search;
214             search.reserve(3);
215             QVector<QByteArray> replacements;
216             replacements.reserve(3);
217             search << "From "
218                    << "from "
219                    << "-";
220             replacements << "From=20"
221                          << "from=20"
222                          << "=2D";
223 
224             if (d->content->contentTransferEncoding()->encoding() == KMime::Headers::CE7Bit) {
225                 for (int i = 0, total = search.size(); i < total; ++i) {
226                     const auto pos = body.indexOf(search[i]);
227                     if (pos == 0 || (pos > 0 && body.at(pos - 1) == '\n')) {
228                         changed = true;
229                         break;
230                     }
231                 }
232                 if (changed) {
233                     d->content->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
234                     d->content->assemble();
235                     body = d->content->encodedBody();
236                 }
237             }
238 
239             for (int i = 0; i < search.size(); ++i) {
240                 const auto pos = body.indexOf(search[i]);
241                 if (pos == 0 || (pos > 0 && body.at(pos - 1) == '\n')) {
242                     changed = true;
243                     body.replace(pos, search[i].size(), replacements[i]);
244                 }
245             }
246 
247             if (changed) {
248                 qCDebug(MESSAGECOMPOSER_LOG) << "Content changed";
249                 d->content->setBody(body);
250                 d->content->contentTransferEncoding()->setDecoded(false);
251             }
252         }
253 
254         content = KMime::LFtoCRLF(d->content->encodedContent());
255     } else { // SMimeOpaque doesn't need LFtoCRLF, else it gets munged
256         content = d->content->encodedContent();
257     }
258 
259     QGpgME::SignJob *job(proto->signJob(!d->binaryHint(d->format), d->format == Kleo::InlineOpenPGPFormat));
260     QObject::connect(
261         job,
262         &QGpgME::SignJob::result,
263         this,
264         [this, d](const GpgME::SigningResult &result, const QByteArray &signature, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) {
265             Q_UNUSED(auditLogAsHtml)
266             Q_UNUSED(auditLogError)
267             if (result.error().code()) {
268                 qCDebug(MESSAGECOMPOSER_LOG) << "signing failed:" << result.error().asString();
269                 //        job->showErrorDialog( globalPart()->parentWidgetForGui() );
270                 setError(result.error().code());
271                 setErrorText(QString::fromLocal8Bit(result.error().asString()));
272                 emitResult();
273                 return;
274             }
275 
276             QByteArray signatureHashAlgo = result.createdSignature(0).hashAlgorithmAsString();
277             d->resultContent = MessageComposer::Util::composeHeadersAndBody(d->content, signature, d->format, true, signatureHashAlgo);
278 
279             emitResult();
280         });
281 
282     const auto error = job->start(d->signers, content, d->signingMode(d->format));
283     if (error.code()) {
284         job->deleteLater();
285         setError(error.code());
286         setErrorText(QString::fromLocal8Bit(error.asString()));
287         emitResult();
288     }
289 }
290