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

KDE's Doxygen guidelines are available online.