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 
36 using namespace Kleo::Class;
37 
38 namespace
39 {
40 
41 const unsigned int ExamineContentHint = 0x8000;
42 
43 static 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 
67 static 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 
85 static 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 
99 static const unsigned int defaultClassification = NoClass;
100 
101 template<typename T>
102 class asKeyValueRange
103 {
104 public:
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 
119 private:
120  T &m_data;
121 };
122 }
123 
124 unsigned 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 
136 static 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
144 static 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 
167 static unsigned int classifyExtension(const QFileInfo &fi)
168 {
169  return classifications.value(fi.suffix(), defaultClassification);
170 }
171 
172 unsigned 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 
205 unsigned 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 
214 QString 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 */
260 QString 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 */
277 QStringList 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
292 static 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 */
303 QString 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 */
323 QString 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 
337 bool Kleo::isFingerprint(const QString &fpr)
338 {
339  static QRegularExpression fprRegex(QStringLiteral("[0-9a-fA-F]{40}"));
340  return fprRegex.match(fpr).hasMatch();
341 }
342 
343 bool 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 }
QString anchoredPattern(const QString &expression)
QString completeSuffix() const const
int size() const const
Type type(const QSqlDatabase &db)
QString chopped(int len) const const
void push_back(const T &value)
KCALUTILS_EXPORT QString mimeType()
const QList< QKeySequence > & begin()
QString suffix() const const
bool exists() const const
bool exists() const const
KEDUVOCDOCUMENT_EXPORT QStringList fileNames(const QString &language=QString())
bool empty() const const
QString join(const QString &separator) const const
QString fileName() const const
QString & remove(int position, int n)
QMimeType mimeTypeForFile(const QString &fileName, QMimeDatabase::MatchMode mode) const const
T & front()
const QList< QKeySequence > & end()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Thu Feb 15 2024 03:56:14 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.