ify_8cpp_source

Search for usage in LXR

classify.cpp
1/* -*- mode: c++; c-basic-offset:4 -*-
2 utils/classify.cpp
3
4 This file is part of Kleopatra, the KDE keymanager
5 SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9
10#include <config-libkleo.h>
11
12#include "classify.h"
13
14#include "algorithm.h"
15#include "classifyconfig.h"
16
17#include <libkleo/checksumdefinition.h>
18
19#include <libkleo_debug.h>
20
21#include <QGpgME/DataProvider>
22
23#include <QByteArrayMatcher>
24#include <QFile>
25#include <QFileInfo>
26#include <QMap>
27#include <QMimeDatabase>
28#include <QRegularExpression>
29#include <QString>
30
31#include <gpgme++/data.h>
32
33#include <functional>
34#include <iterator>
35
36using namespace Kleo::Class;
37using namespace Qt::Literals::StringLiterals;
38
39namespace
40{
41
42const unsigned int ExamineContentHint = 0x8000;
43
44static const QMap<QString, unsigned int> classifications{
45 // using QMap to keep ordering by extension which incidentally is also the prioritized order for outputFileExtension()
46 {QStringLiteral("arl"), Kleo::Class::CMS | Binary | CertificateRevocationList},
47 {QStringLiteral("asc"), Kleo::Class::OpenPGP | Ascii | OpaqueSignature | DetachedSignature | CipherText | AnyCertStoreType | ExamineContentHint},
48 {QStringLiteral("cer"), Kleo::Class::CMS | Binary | Certificate},
49 {QStringLiteral("crl"), Kleo::Class::CMS | Binary | CertificateRevocationList},
50 {QStringLiteral("crt"), Kleo::Class::CMS | Binary | Certificate},
51 {QStringLiteral("der"), Kleo::Class::CMS | Binary | Certificate | CertificateRevocationList},
52 {QStringLiteral("eml"), Kleo::Class::MimeFile | Ascii},
53 {QStringLiteral("gpg"), Kleo::Class::OpenPGP | Binary | OpaqueSignature | CipherText | AnyCertStoreType | ExamineContentHint},
54 {QStringLiteral("mim"), Kleo::Class::MimeFile | Ascii},
55 {QStringLiteral("mime"), Kleo::Class::MimeFile | Ascii},
56 {QStringLiteral("mbox"), Kleo::Class::MimeFile | Ascii},
57 {QStringLiteral("p10"), Kleo::Class::CMS | Ascii | CertificateRequest},
58 {QStringLiteral("p12"), Kleo::Class::CMS | Binary | ExportedPSM},
59 {QStringLiteral("p7c"), Kleo::Class::CMS | Binary | Certificate},
60 {QStringLiteral("p7m"), Kleo::Class::CMS | AnyFormat | CipherText},
61 {QStringLiteral("p7s"), Kleo::Class::CMS | AnyFormat | AnySignature},
62 {QStringLiteral("pem"), Kleo::Class::CMS | Ascii | AnyType | ExamineContentHint},
63 {QStringLiteral("pfx"), Kleo::Class::CMS | Binary | Certificate},
64 {QStringLiteral("pgp"), Kleo::Class::OpenPGP | Binary | OpaqueSignature | CipherText | AnyCertStoreType | ExamineContentHint},
65 {QStringLiteral("sig"), Kleo::Class::OpenPGP | AnyFormat | DetachedSignature},
66};
67
68static const QHash<GpgME::Data::Type, unsigned int> gpgmeTypeMap{
69 // clang-format off
70 {GpgME::Data::PGPSigned, Kleo::Class::OpenPGP | OpaqueSignature },
71 /* PGPOther might be just an unencrypted unsigned pgp message. Decrypt
72 * would yield the plaintext anyway so for us this is CipherText. */
73 {GpgME::Data::PGPOther, Kleo::Class::OpenPGP | CipherText },
74 {GpgME::Data::PGPKey, Kleo::Class::OpenPGP | Certificate },
75 {GpgME::Data::CMSSigned, Kleo::Class::CMS | AnySignature },
76 {GpgME::Data::CMSEncrypted, Kleo::Class::CMS | CipherText },
77 /* See PGPOther */
78 {GpgME::Data::CMSOther, Kleo::Class::CMS | CipherText },
79 {GpgME::Data::X509Cert, Kleo::Class::CMS | Certificate },
80 {GpgME::Data::PKCS12, Kleo::Class::CMS | Binary | ExportedPSM },
81 {GpgME::Data::PGPEncrypted, Kleo::Class::OpenPGP | CipherText },
82 {GpgME::Data::PGPSignature, Kleo::Class::OpenPGP | DetachedSignature},
83 // clang-format on
84};
85
86static const QSet<QString> mimeFileNames{
87 /* KMail standard name */
88 QStringLiteral("msg.asc"),
89 QStringLiteral("smime.p7m"),
90 QStringLiteral("openpgp-encrypted-message.asc"),
91 /* Old names of internal GpgOL attachments newer versions
92 * should use .mime file ending as it is connected with
93 * Kleopatra. */
94 QStringLiteral("GpgOL_MIME_structure.txt"),
95 QStringLiteral("GpgOL_MIME_structure.mime"),
96 /* This is gpgtools take on the filename */
97 QStringLiteral("OpenPGP encrypted message.asc"),
98};
99
100static const unsigned int defaultClassification = NoClass;
101
102template<typename T>
103class asKeyValueRange
104{
105public:
106 asKeyValueRange(T &data)
107 : m_data{data}
108 {
109 }
110
111 auto begin()
112 {
113 return m_data.keyValueBegin();
114 }
115 auto end()
116 {
117 return m_data.keyValueEnd();
118 }
119
120private:
121 T &m_data;
122};
123}
124
125unsigned int Kleo::classify(const QStringList &fileNames)
126{
127 if (fileNames.empty()) {
128 return 0;
129 }
130 unsigned int result = classify(fileNames.front());
131 for (const QString &fileName : fileNames) {
132 result &= classify(fileName);
133 }
134 return result;
135}
136
137static bool mimeTypeInherits(const QMimeType &mimeType, const QString &mimeTypeName)
138{
139 // inherits is expensive on an invalid mimeType
140 return mimeType.isValid() && mimeType.inherits(mimeTypeName);
141}
142
143/// Detect either a complete mail file (e.g. mbox or eml file) or a encrypted attachment
144/// corresponding to a mail file
145static bool isMailFile(const QFileInfo &fi)
146{
147 static const QRegularExpression attachmentNumbering{QStringLiteral(R"(\‍([0-9]+\))")};
148 const auto fileName = fi.fileName().remove(attachmentNumbering);
149
150 if (mimeFileNames.contains(fileName)) {
151 return true;
152 }
153
154 {
155 Kleo::ClassifyConfig classifyConfig;
156
157 if (classifyConfig.p7mWithoutExtensionAreEmail() && fileName.endsWith(QStringLiteral(".p7m")) && fi.completeSuffix() == fi.suffix()) {
158 // match "myfile.p7m" but not "myfile.pdf.p7m"
159 return true;
160 }
161 }
162
163 QMimeDatabase mimeDatabase;
164 const auto mimeType = mimeDatabase.mimeTypeForFile(fi);
165 return mimeTypeInherits(mimeType, QStringLiteral("message/rfc822")) || mimeTypeInherits(mimeType, QStringLiteral("application/mbox"));
166}
167
168static unsigned int classifyExtension(const QFileInfo &fi)
169{
170 return classifications.value(fi.suffix(), defaultClassification);
171}
172
173unsigned int Kleo::classify(const QString &filename)
174{
175 const QFileInfo fi(filename);
176
177 if (!fi.exists()) {
178 return 0;
179 }
180
181 if (isMailFile(fi)) {
182 return Kleo::Class::MimeFile | Ascii;
183 }
184
185 QFile file(filename);
186 /* The least reliable but always available classification */
187 const unsigned int extClass = classifyExtension(fi);
188 if (!file.open(QIODevice::ReadOnly)) {
189 qCDebug(LIBKLEO_LOG) << "Failed to open file: " << filename << " for classification.";
190 return extClass;
191 }
192
193 /* More reliable */
194 const unsigned int contentClass = classifyContent(file.read(4096));
195 if (contentClass != defaultClassification) {
196 qCDebug(LIBKLEO_LOG) << "Classified based on content as:" << contentClass;
197 return contentClass;
198 }
199
200 /* Probably some X509 Stuff that GpgME in its wisdom does not handle. Again
201 * file extension is probably more reliable as the last resort. */
202 qCDebug(LIBKLEO_LOG) << "No classification based on content.";
203 return extClass;
204}
205
206unsigned int Kleo::classifyContent(const QByteArray &data)
207{
208 QGpgME::QByteArrayDataProvider dp(data);
209 GpgME::Data gpgmeData(&dp);
210 GpgME::Data::Type type = gpgmeData.type();
211
212 return gpgmeTypeMap.value(type, defaultClassification);
213}
214
215QString Kleo::printableClassification(unsigned int classification)
216{
217 QStringList parts;
218 if (classification & Kleo::Class::CMS) {
219 parts.push_back(QStringLiteral("CMS"));
220 }
221 if (classification & Kleo::Class::OpenPGP) {
222 parts.push_back(QStringLiteral("OpenPGP"));
223 }
224 if (classification & Kleo::Class::Binary) {
225 parts.push_back(QStringLiteral("Binary"));
226 }
227 if (classification & Kleo::Class::Ascii) {
228 parts.push_back(QStringLiteral("Ascii"));
229 }
230 if (classification & Kleo::Class::DetachedSignature) {
231 parts.push_back(QStringLiteral("DetachedSignature"));
232 }
233 if (classification & Kleo::Class::OpaqueSignature) {
234 parts.push_back(QStringLiteral("OpaqueSignature"));
235 }
236 if (classification & Kleo::Class::ClearsignedMessage) {
237 parts.push_back(QStringLiteral("ClearsignedMessage"));
238 }
239 if (classification & Kleo::Class::CipherText) {
240 parts.push_back(QStringLiteral("CipherText"));
241 }
242 if (classification & Kleo::Class::Certificate) {
243 parts.push_back(QStringLiteral("Certificate"));
244 }
245 if (classification & Kleo::Class::ExportedPSM) {
246 parts.push_back(QStringLiteral("ExportedPSM"));
247 }
248 if (classification & Kleo::Class::CertificateRequest) {
249 parts.push_back(QStringLiteral("CertificateRequest"));
250 }
251 if (classification & Kleo::Class::MimeFile) {
252 parts.push_back(QStringLiteral("MimeFile"));
253 }
254 return parts.join(QLatin1StringView(", "));
255}
256
257/*!
258 \return the data file that corresponds to the signature file \a
259 signatureFileName, or QString(), if no such file can be found.
260*/
261QString Kleo::findSignedData(const QString &signatureFileName)
262{
263 if (!mayBeDetachedSignature(signatureFileName)) {
264 return QString();
265 }
266
267 const QFileInfo fi{signatureFileName};
268 const QString baseName = signatureFileName.chopped(fi.suffix().size() + 1);
269 return QFile::exists(baseName) ? baseName : QString();
270}
271
272/*!
273 \return all (existing) candidate signature files for \a signedDataFileName
274
275 Note that there can very well be more than one such file, e.g. if
276 the same data file was signed by both CMS and OpenPGP certificates.
277*/
278QStringList Kleo::findSignatures(const QString &signedDataFileName)
279{
280 QStringList result;
281 for (const auto &[extension, classification] : asKeyValueRange(classifications)) {
282 if (classification & DetachedSignature) {
283 const QString candidate = signedDataFileName + QLatin1Char('.') + extension;
284 if (QFile::exists(candidate)) {
285 result.push_back(candidate);
286 }
287 }
288 }
289 return result;
290}
291
292#ifdef Q_OS_WIN
293static QString stripOutlookAttachmentNumbering(const QString &s)
294{
295 static const QRegularExpression attachmentNumbering{QStringLiteral(R"(\s\‍([0-9]+\)$)")};
296 return QString{s}.remove(attachmentNumbering);
297}
298#endif
299
300/*!
301 \return the (likely) output filename for \a inputFileName, or
302 "inputFileName.out" if none can be determined.
303*/
304QString Kleo::outputFileName(const QString &inputFileName)
305{
306 const QFileInfo fi(inputFileName);
307 const QString suffix = fi.suffix();
308
309 if (classifications.find(suffix) == std::cend(classifications)) {
310 return inputFileName + QLatin1StringView(".out");
311 } else {
312#ifdef Q_OS_WIN
313 return stripOutlookAttachmentNumbering(inputFileName.chopped(suffix.size() + 1));
314#else
315 return inputFileName.chopped(suffix.size() + 1);
316#endif
317 }
318}
319
320/*!
321 \return the commonly used extension for files of type
322 \a classification, or NULL if none such exists.
323*/
324QString Kleo::outputFileExtension(unsigned int classification, bool usePGPFileExt)
325{
326 if (usePGPFileExt && (classification & Class::OpenPGP) && (classification & Class::Binary)) {
327 return QStringLiteral("pgp");
328 }
329
330 for (const auto &[extension, classification_] : asKeyValueRange(classifications)) {
331 if ((classification_ & classification) == classification) {
332 return extension;
333 }
334 }
335 return {};
336}
337
338bool Kleo::isFingerprint(const QString &fpr)
339{
340 static const QRegularExpression fprRegex{QRegularExpression::anchoredPattern(u"[0-9a-fA-F]+"_s)};
341 return (fpr.size() == 40 || fpr.size() == 64) && fprRegex.match(fpr).hasMatch();
342}
343
344bool Kleo::isChecksumFile(const QString &file)
345{
346 static bool initialized;
347 static QList<QRegularExpression> patterns;
348 const QFileInfo fi(file);
349 if (!fi.exists()) {
350 return false;
351 }
352 if (!initialized) {
353 const auto getChecksumDefinitions = ChecksumDefinition::getChecksumDefinitions();
354 for (const std::shared_ptr<ChecksumDefinition> &cd : getChecksumDefinitions) {
355 if (cd) {
356 const auto patternsList = cd->patterns();
357 for (const QString &pattern : patternsList) {
358#ifdef Q_OS_WIN
360#else
362#endif
363 }
364 }
365 }
366 initialized = true;
367 }
368
369 const QString fileName = fi.fileName();
370 for (const QRegularExpression &pattern : std::as_const(patterns)) {
371 if (pattern.match(fileName).hasMatch()) {
372 return true;
373 }
374 }
375 return false;
376}
KCALUTILS_EXPORT QString mimeType()
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
const QList< QKeySequence > & begin()
KEDUVOCDOCUMENT_EXPORT QStringList fileNames(const QString &language=QString())
bool exists() const const
QString completeSuffix() const const
bool exists(const QString &path)
QString fileName() const const
QString suffix() const const
bool empty() const const
reference front()
void push_back(parameter_type value)
QMimeType mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const const
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
QString anchoredPattern(QStringView expression)
bool hasMatch() const const
QString chopped(qsizetype len) const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
qsizetype size() const const
QString join(QChar separator) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:29:01 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.