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