KNewStuff

security.cpp
1 /*
2  This file is part of KNewStuff2.
3  SPDX-FileCopyrightText: 2004, 2005 Andras Mantia <[email protected]>
4  SPDX-FileCopyrightText: 2007 Josef Spillner <[email protected]>
5 
6  SPDX-License-Identifier: LGPL-2.1-or-later
7 */
8 
9 // app includes
10 #include "security.h"
11 #include "question.h"
12 
13 // qt includes
14 #include <QFile>
15 #include <QFileInfo>
16 #include <QStringList>
17 #include <QTextStream>
18 #include <QTimer>
19 #include <qstandardpaths.h>
20 
21 #include <QCryptographicHash>
22 
23 // kde includes
24 #include <KLocalizedString>
25 
26 using namespace KNSCore;
27 
28 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 31)
29 static QString gpgExecutable()
30 {
31  QString gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg"));
32  if (gpgExe.isEmpty()) {
33  gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg2"));
34  }
35  if (gpgExe.isEmpty()) {
36  return QStringLiteral("gpg");
37  }
38  return gpgExe;
39 }
40 
41 Security::Security()
42 {
43  m_keysRead = false;
44  m_gpgRunning = false;
45  readKeys();
47 }
48 
49 Security::~Security()
50 {
51 }
52 
54 {
55  if (m_gpgRunning) {
57  return;
58  }
59  m_runMode = List;
60  m_keys.clear();
61  m_process = new QProcess();
62  QStringList arguments;
63  arguments << QStringLiteral("--no-secmem-warning") << QStringLiteral("--no-tty") << QStringLiteral("--with-colon") << QStringLiteral("--list-keys");
64  connect(m_process, &QProcess::finished, this, &Security::slotFinished);
65  connect(m_process, &QProcess::readyReadStandardOutput, this, &Security::slotReadyReadStandardOutput);
66  m_process->start(gpgExecutable(), arguments);
67  if (!m_process->waitForStarted()) {
68  Q_EMIT signalError(
69  i18n("<qt>Cannot start <i>gpg</i> and retrieve the available keys. Make sure that <i>gpg</i> is installed, otherwise verification of downloaded "
70  "resources will not be possible.</qt>"));
71  delete m_process;
72  m_process = nullptr;
73  } else {
74  m_gpgRunning = true;
75  }
76 }
77 
79 {
80  if (m_gpgRunning) {
82  return;
83  }
84  m_runMode = ListSecret;
85  m_process = new QProcess();
86  QStringList arguments;
87  arguments << QStringLiteral("--no-secmem-warning") << QStringLiteral("--no-tty") << QStringLiteral("--with-colon") << QStringLiteral("--list-secret-keys");
88  connect(m_process, &QProcess::finished, this, &Security::slotFinished);
89  connect(m_process, &QProcess::readyReadStandardOutput, this, &Security::slotReadyReadStandardOutput);
90  m_process->start(gpgExecutable(), arguments);
91  if (!m_process->waitForStarted()) {
92  delete m_process;
93  m_process = nullptr;
94  } else {
95  m_gpgRunning = true;
96  }
97 }
98 
99 void Security::slotFinished(int exitCode, QProcess::ExitStatus exitStatus)
100 {
101  if (exitStatus != QProcess::NormalExit) {
102  m_gpgRunning = false;
103  delete m_process;
104  m_process = nullptr;
105  return;
106  }
107  switch (m_runMode) {
108  case ListSecret:
109  m_keysRead = true;
110  break;
111  case Verify:
112  Q_EMIT validityResult(m_result);
113  break;
114  case Sign:
115  Q_EMIT fileSigned(m_result);
116  break;
117  }
118  m_gpgRunning = false;
119  delete m_process;
120  m_process = nullptr;
121 
122  Q_UNUSED(exitCode)
123 }
124 
125 void Security::slotReadyReadStandardOutput()
126 {
127  QString data;
128  while (m_process->canReadLine()) {
129  data = QString::fromLocal8Bit(m_process->readLine());
130  switch (m_runMode) {
131  case List:
132  case ListSecret:
133  if (data.startsWith(QLatin1String("pub")) || data.startsWith(QLatin1String("sec"))) {
134  KeyStruct key;
135  if (data.startsWith(QLatin1String("pub"))) {
136  key.secret = false;
137  } else {
138  key.secret = true;
139  }
140  QStringList line = data.split(QLatin1Char(':'), Qt::KeepEmptyParts);
141  key.id = line[4];
142  QString shortId = key.id.right(8);
143  QString trustStr = line[1];
144  key.trusted = false;
145  if (trustStr == QLatin1Char('u') || trustStr == QLatin1Char('f')) {
146  key.trusted = true;
147  }
148  data = line[9];
149  key.mail = data.section(QLatin1Char('<'), -1, -1);
150  key.mail.chop(1);
151  key.name = data.section(QLatin1Char('<'), 0, 0);
152  if (key.name.contains(QLatin1Char('('))) {
153  key.name = key.name.section(QLatin1Char('('), 0, 0);
154  }
155  m_keys[shortId] = key;
156  }
157  break;
158  case Verify:
159  data = data.section(QLatin1Char(']'), 1, -1).trimmed();
160  if (data.startsWith(QLatin1String("GOODSIG"))) {
161  m_result &= SIGNED_BAD_CLEAR;
162  m_result |= SIGNED_OK;
163  QString id = data.section(QLatin1Char(' '), 1, 1).right(8);
164  if (!m_keys.contains(id)) {
165  m_result |= UNKNOWN;
166  } else {
167  m_signatureKey = m_keys[id];
168  }
169  } else if (data.startsWith(QLatin1String("NO_PUBKEY"))) {
170  m_result &= SIGNED_BAD_CLEAR;
171  m_result |= UNKNOWN;
172  } else if (data.startsWith(QLatin1String("BADSIG"))) {
173  m_result |= SIGNED_BAD;
174  QString id = data.section(QLatin1Char(' '), 1, 1).right(8);
175  if (!m_keys.contains(id)) {
176  m_result |= UNKNOWN;
177  } else {
178  m_signatureKey = m_keys[id];
179  }
180  } else if (data.startsWith(QLatin1String("TRUST_ULTIMATE"))) {
181  m_result &= SIGNED_BAD_CLEAR;
182  m_result |= TRUSTED;
183  }
184  break;
185 
186  case Sign:
187  if (data.contains(QLatin1String("passphrase.enter"))) {
188  KeyStruct key = m_keys[m_secretKey];
189  Question question(Question::PasswordQuestion);
190  question.setQuestion(
191  i18n("<qt>Enter passphrase for key <b>0x%1</b>, belonging to<br /><i>%2&lt;%3&gt;</i><br />:</qt>", m_secretKey, key.name, key.mail));
192  if (question.ask() == Question::ContinueResponse) {
193  m_process->write(question.response().toLocal8Bit() + '\n');
194  } else {
195  m_result |= BAD_PASSPHRASE;
196  m_process->kill();
197  return;
198  }
199  } else if (data.contains(QLatin1String("BAD_PASSPHRASE"))) {
200  m_result |= BAD_PASSPHRASE;
201  }
202  break;
203  }
204  }
205 }
206 
207 void Security::checkValidity(const QString &filename)
208 {
209  m_fileName = filename;
211 }
212 
214 {
215  if (!m_keysRead || m_gpgRunning) {
217  return;
218  }
219  if (m_keys.isEmpty()) {
221  return;
222  }
223 
224  m_result = 0;
225  m_runMode = Verify;
226  QFileInfo f(m_fileName);
227  // check the MD5 sum
228  QString md5sum;
230  QFile file(m_fileName);
231  if (!m_fileName.isEmpty() && file.open(QIODevice::ReadOnly)) {
232  context.reset();
233  context.addData(&file);
234  md5sum = QString::fromLatin1(context.result().toHex());
235  file.close();
236  }
237  file.setFileName(f.path() + QStringLiteral("/md5sum"));
238  if (file.open(QIODevice::ReadOnly)) {
239  QByteArray md5sum_file;
240  file.readLine(md5sum_file.data(), 50);
241  if (!md5sum_file.isEmpty() && QString::fromLatin1(md5sum_file).startsWith(md5sum)) {
242  m_result |= MD5_OK;
243  }
244  file.close();
245  }
246  m_result |= SIGNED_BAD;
247  m_signatureKey.id = QLatin1String("");
248  m_signatureKey.name = QLatin1String("");
249  m_signatureKey.mail = QLatin1String("");
250  m_signatureKey.trusted = false;
251 
252  // verify the signature
253  m_process = new QProcess();
254  QStringList arguments;
255  arguments << QStringLiteral("--no-secmem-warning") << QStringLiteral("--status-fd=2") << QStringLiteral("--command-fd=0") << QStringLiteral("--verify")
256  << f.path() + QStringLiteral("/signature") << m_fileName;
257  connect(m_process, &QProcess::finished, this, &Security::slotFinished);
258  connect(m_process, &QProcess::readyReadStandardOutput, this, &Security::slotReadyReadStandardOutput);
259  m_process->start(gpgExecutable(), arguments);
260  if (m_process->waitForStarted()) {
261  m_gpgRunning = true;
262  } else {
263  Q_EMIT signalError(
264  i18n("<qt>Cannot start <i>gpg</i> and check the validity of the file. Make sure that <i>gpg</i> is installed, otherwise verification of downloaded "
265  "resources will not be possible.</qt>"));
267  delete m_process;
268  m_process = nullptr;
269  }
270 }
271 
272 void Security::signFile(const QString &fileName)
273 {
274  m_fileName = fileName;
275  slotSignFile();
276 }
277 
279 {
280  if (!m_keysRead || m_gpgRunning) {
282  return;
283  }
284 
285  QStringList secretKeys;
286  for (QMap<QString, KeyStruct>::Iterator it = m_keys.begin(); it != m_keys.end(); ++it) {
287  if (it.value().secret) {
288  secretKeys.append(it.key());
289  }
290  }
291 
292  if (secretKeys.isEmpty()) {
293  Q_EMIT fileSigned(-1);
294  return;
295  }
296 
297  m_result = 0;
298  QFileInfo f(m_fileName);
299 
300  // create the MD5 sum
301  QString md5sum;
303  QFile file(m_fileName);
304  if (file.open(QIODevice::ReadOnly)) {
305  context.reset();
306  context.addData(&file);
307  md5sum = QString::fromLatin1(context.result().toHex());
308  file.close();
309  }
310  file.setFileName(f.path() + QStringLiteral("/md5sum"));
311  if (file.open(QIODevice::WriteOnly)) {
312  QTextStream stream(&file);
313  stream << md5sum;
314  m_result |= MD5_OK;
315  file.close();
316  }
317 
318  if (secretKeys.count() > 1) {
319  Question question(Question::SelectFromListQuestion);
320  question.setQuestion(i18n("Key used for signing:"));
321  question.setTitle(i18n("Select Signing Key"));
322  question.setList(secretKeys);
323  if (question.ask() == Question::OKResponse) {
324  m_secretKey = question.response();
325  } else {
326  // emit an error to be forwarded to the user for selecting a signing key...
327  Q_EMIT fileSigned(0);
328  return;
329  }
330  } else {
331  m_secretKey = secretKeys[0];
332  }
333 
334  // verify the signature
335  m_process = new QProcess();
336  QStringList arguments;
337  arguments << QStringLiteral("--no-secmem-warning") << QStringLiteral("--status-fd=2") << QStringLiteral("--command-fd=0") << QStringLiteral("--no-tty")
338  << QStringLiteral("--detach-sign") << QStringLiteral("-u") << m_secretKey << QStringLiteral("-o") << f.path() + QStringLiteral("/signature")
339  << m_fileName;
340  connect(m_process, &QProcess::finished, this, &Security::slotFinished);
341  connect(m_process, &QProcess::readyReadStandardOutput, this, &Security::slotReadyReadStandardOutput);
342  m_runMode = Sign;
343  m_process->start(gpgExecutable(), arguments);
344  if (m_process->waitForStarted()) {
345  m_gpgRunning = true;
346  } else {
347  Q_EMIT signalError(
348  i18n("<qt>Cannot start <i>gpg</i> and sign the file. Make sure that <i>gpg</i> is installed, otherwise signing of the resources will not be "
349  "possible.</qt>"));
350  Q_EMIT fileSigned(0);
351  delete m_process;
352  m_process = nullptr;
353  }
354 }
355 #endif
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
void append(const T &value)
void signFile(const QString &fileName)
Creates a signature and an md5sum file for the fileName and packs everything into a gzipped tarball.
Definition: security.cpp:272
QString path() const const
QString section(QChar sep, int start, int end, QString::SectionFlags flags) const const
bool contains(const Key &key) const const
void addData(const char *data, int length)
void checkValidity(const QString &fileName)
Verifies the integrity and the signature of a tarball file.
Definition: security.cpp:207
Q_EMITQ_EMIT
virtual bool open(QIODevice::OpenMode mode) override
void clear()
void slotCheckValidity()
Verifies the integrity and the signature of a tarball file (see m_fileName).
Definition: security.cpp:213
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
int count(const T &value) const const
@ SIGNED_OK
The MD5 sum check is OK.
Definition: security.h:78
QMap::iterator begin()
QString trimmed() const const
void chop(int n)
QByteArray result() const const
void finished(int exitCode)
void readSecretKeys()
Reads the available secret keys.
Definition: security.cpp:78
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void readyReadStandardOutput()
@ SIGNED_BAD
The file is signed with a good signature.
Definition: security.h:79
QString findExecutable(const QString &executableName, const QStringList &paths)
void validityResult(int result)
Sent when the validity check is done.
QMap::iterator end()
void slotSignFile()
Creates a signature and an md5sum file for the m_fileName and packs everything into a gzipped tarball...
Definition: security.cpp:278
QString i18n(const char *text, const TYPE &arg...)
QString fromLocal8Bit(const char *str, int size)
KeepEmptyParts
A way to ask a user a question from inside a GUI-less library (like KNewStuffCore)
Definition: question.h:46
bool isEmpty() const const
bool waitForStarted(int msecs)
void kill()
void setFileName(const QString &name)
@ BAD_PASSPHRASE
used to clear the SIGNED_BAD flag
Definition: security.h:83
bool isEmpty() const const
virtual void close() override
qint64 readLine(char *data, qint64 maxSize)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
@ UNKNOWN
The signature is trusted.
Definition: security.h:81
QByteArray toHex() const const
bool isEmpty() const const
@ TRUSTED
The file is signed with a bad signature.
Definition: security.h:80
QString right(int n) const const
QString fromLatin1(const char *str, int size)
void readKeys()
Reads the available public keys.
Definition: security.cpp:53
virtual bool canReadLine() const const override
@ SIGNED_BAD_CLEAR
The key is unknown.
Definition: security.h:82
Contains the core functionality for handling interaction with NewStuff providers.
void fileSigned(int result)
Sent when the signing is done.
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
char * data()
qint64 write(const char *data, qint64 maxSize)
bool isEmpty() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Mar 26 2023 04:18:34 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.