1 /*
2     SPDX-FileCopyrightText: 2009, 2010, 2012, 2014, 2016, 2018 Rolf Eike Beer <kde@opensource.sf-tec.de>
3     SPDX-License-Identifier: GPL-2.0-or-later
4 */
5 
6 #include "kgpgtransactionprivate.h"
7 #include "kgpgtransaction.h"
8 #include "kgpg_transactions_debug.h"
9 #include "kgpg_general_debug.h"
10 
11 #include <QPointer>
12 #include <QWidget>
13 
14 #include <KLocalizedString>
15 #include <KIO/RenameDialog>
16 
KGpgTransactionPrivate(KGpgTransaction * parent,bool allowChaining)17 KGpgTransactionPrivate::KGpgTransactionPrivate(KGpgTransaction *parent, bool allowChaining)
18 	: m_parent(parent),
19 	m_process(new GPGProc()),
20 	m_inputTransaction(nullptr),
21 	m_newPasswordDialog(nullptr),
22 	m_passwordDialog(nullptr),
23 	m_success(KGpgTransaction::TS_OK),
24 	m_tries(3),
25 	m_chainingAllowed(allowChaining),
26 	m_inputProcessDone(false),
27 	m_inputProcessResult(KGpgTransaction::TS_OK),
28 	m_ownProcessFinished(false),
29 	m_quitTries(0)
30 {
31 	connect(m_process, &GPGProc::readReady, this, &KGpgTransactionPrivate::slotReadReady);
32 	connect(m_process, &GPGProc::processExited, this, &KGpgTransactionPrivate::slotProcessExited);
33 	connect(m_process, &GPGProc::started, this, &KGpgTransactionPrivate::slotProcessStarted);
34 }
35 
~KGpgTransactionPrivate()36 KGpgTransactionPrivate::~KGpgTransactionPrivate()
37 {
38 	if (m_newPasswordDialog) {
39 		m_newPasswordDialog->close();
40 		m_newPasswordDialog->deleteLater();
41 	}
42 	if (m_process->state() == QProcess::Running) {
43 		m_process->closeWriteChannel();
44 		m_process->terminate();
45 	}
46 	delete m_inputTransaction;
47 	delete m_process;
48 }
49 
50 void
slotReadReady()51 KGpgTransactionPrivate::slotReadReady()
52 {
53 	QString line;
54 	QPointer<GPGProc> process(m_process);
55 	QPointer<KGpgTransaction> par(m_parent);
56 
57 	while (!process.isNull() && (m_process->readln(line, true) >= 0)) {
58 		if (m_quitTries)
59 			m_quitLines << line;
60 #ifdef KGPG_DEBUG_TRANSACTIONS
61 		qCDebug(KGPG_LOG_TRANSACTIONS) << m_parent << line;
62 #endif /* KGPG_DEBUG_TRANSACTIONS */
63 
64 		static const QString getBool = QLatin1String("[GNUPG:] GET_BOOL ");
65 		if (keyConsidered(line)) {
66 			// already handled by keyConsidered - skip the line
67 		} else if (line.startsWith(QLatin1String("[GNUPG:] USERID_HINT "))) {
68 			m_parent->addIdHint(line);
69 		} else if (line.startsWith(QLatin1String("[GNUPG:] BAD_PASSPHRASE "))) {
70 			// the MISSING_PASSPHRASE line comes first, in that case ignore a
71 			// following BAD_PASSPHRASE
72 			if (m_success != KGpgTransaction::TS_USER_ABORTED)
73 				m_success = KGpgTransaction::TS_BAD_PASSPHRASE;
74 		} else if (line.startsWith(QLatin1String("[GNUPG:] GET_HIDDEN passphrase.enter"))) {
75 			const bool goOn = m_parent->passphraseRequested();
76 
77 			// Check if the object was deleted while waiting for the result
78 			if (!goOn || par.isNull())
79 				return;
80 
81 		} else if (line.startsWith(QLatin1String("[GNUPG:] GOOD_PASSPHRASE"))) {
82 			Q_EMIT m_parent->statusMessage(i18n("Got Passphrase"));
83 
84 			if (m_passwordDialog != nullptr) {
85 				m_passwordDialog->close();
86 				m_passwordDialog->deleteLater();
87 				m_passwordDialog = nullptr;
88 			}
89 
90 			if (m_parent->passphraseReceived()) {
91 				// signal GnuPG that there will be no further input and it can
92 				// begin sending output.
93 				m_process->closeWriteChannel();
94 			}
95 
96 		} else if (line.startsWith(getBool)) {
97 			const QString question = line.mid(getBool.length());
98 
99 			KGpgTransaction::ts_boolanswer answer;
100 
101 			if (question.startsWith(QLatin1String("openfile.overwrite.okay"))) {
102 				m_overwriteUrl.clear();
103 				answer = m_parent->confirmOverwrite(m_overwriteUrl);
104 
105 				if ((answer == KGpgTransaction::BA_UNKNOWN) && !m_overwriteUrl.isEmpty()) {
106 					QPointer<KIO::RenameDialog> over = new KIO::RenameDialog(qobject_cast<QWidget *>(m_parent->parent()),
107 							i18n("File Already Exists"), QUrl(),
108 							m_overwriteUrl, KIO::RenameDialog_Overwrite);
109 
110 					m_overwriteUrl.clear();
111 
112 					switch (over->exec()) {
113 					case KIO::Result_Overwrite:
114 						answer = KGpgTransaction::BA_YES;
115 						break;
116 					case KIO::Result_Rename:
117 						answer = KGpgTransaction::BA_NO;
118 						m_overwriteUrl = over->newDestUrl();
119 						break;
120 					default:
121 						answer = KGpgTransaction::BA_UNKNOWN;
122 						m_parent->setSuccess(KGpgTransaction::TS_USER_ABORTED);
123 						// Close the pipes, otherwise GnuPG will try to answer
124 						// further questions about this file.
125 						m_process->closeWriteChannel();
126 						m_process->closeReadChannel(QProcess::StandardOutput);
127 						break;
128 					}
129 
130 					delete over;
131 
132 					if (answer == KGpgTransaction::BA_UNKNOWN)
133 						continue;
134 				}
135 			} else {
136 				answer = m_parent->boolQuestion(question);
137 			}
138 
139 			switch (answer) {
140 			case KGpgTransaction::BA_YES:
141 				write("YES\n");
142 				break;
143 			case KGpgTransaction::BA_NO:
144 				write("NO\n");
145 				break;
146 			case KGpgTransaction::BA_UNKNOWN:
147 				m_parent->setSuccess(KGpgTransaction::TS_MSG_SEQUENCE);
148 				m_parent->unexpectedLine(line);
149 				sendQuit();
150 			}
151 		} else if (!m_overwriteUrl.isEmpty() && line.startsWith(QLatin1String("[GNUPG:] GET_LINE openfile.askoutname"))) {
152 			write(m_overwriteUrl.toLocalFile().toUtf8() + '\n');
153 			m_overwriteUrl.clear();
154 		} else if (line.startsWith(QLatin1String("[GNUPG:] MISSING_PASSPHRASE"))) {
155 			m_success = KGpgTransaction::TS_USER_ABORTED;
156 		} else if (line.startsWith(QLatin1String("[GNUPG:] CARDCTRL "))) {
157 			// just ignore them, pinentry should handle that
158 		} else {
159 			// all known hints
160 			int i = 0;
161 			bool matched = false;
162 			for (const QString &hintName : hintNames()) {
163 				const KGpgTransaction::ts_hintType h = static_cast<KGpgTransaction::ts_hintType>(i++);
164 				if (!line.startsWith(hintName))
165 					continue;
166 
167 				matched = true;
168 
169 				bool r;
170 				const int skip = hintName.length();
171 				if (line.length() == skip) {
172 					r = m_parent->hintLine(h, QString());
173 				} else {
174 					r = m_parent->hintLine(h, line.mid(skip + 1).trimmed());
175 				}
176 
177 				if (!r) {
178 					m_parent->setSuccess(KGpgTransaction::TS_MSG_SEQUENCE);
179 					sendQuit();
180 				}
181 
182 				break;
183 			}
184 
185 			if (!matched) {
186 				if (m_parent->nextLine(line))
187 					sendQuit();
188 			}
189 		}
190 	}
191 }
192 
193 void
slotProcessExited()194 KGpgTransactionPrivate::slotProcessExited()
195 {
196 	Q_ASSERT(sender() == m_process);
197 
198 	m_ownProcessFinished = true;
199 
200 	if (m_inputProcessDone)
201 		processDone();
202 }
203 
204 void
slotProcessStarted()205 KGpgTransactionPrivate::slotProcessStarted()
206 {
207 	m_parent->postStart();
208 }
209 
210 void
sendQuit(void)211 KGpgTransactionPrivate::sendQuit(void)
212 {
213 	write("quit\n");
214 
215 #ifdef KGPG_DEBUG_TRANSACTIONS
216 	if (m_quitTries == 0)
217 		qCDebug(KGPG_LOG_TRANSACTIONS) << "sending quit";
218 #endif /* KGPG_DEBUG_TRANSACTIONS */
219 
220 	if (m_quitTries++ >= 3) {
221 		qCDebug(KGPG_LOG_GENERAL) << "tried" << m_quitTries << "times to quit the GnuPG session";
222 		qCDebug(KGPG_LOG_GENERAL) << "last input was" << m_quitLines;
223 		qCDebug(KGPG_LOG_GENERAL) << "please file a bug report at https://bugs.kde.org";
224 		m_process->closeWriteChannel();
225 		m_success = KGpgTransaction::TS_MSG_SEQUENCE;
226 	}
227 }
228 
229 void
slotInputTransactionDone(int result)230 KGpgTransactionPrivate::slotInputTransactionDone(int result)
231 {
232 	Q_ASSERT(sender() == m_inputTransaction);
233 
234 	m_inputProcessDone = true;
235 	m_inputProcessResult = result;
236 
237 	if (m_ownProcessFinished)
238 		processDone();
239 }
240 
241 void
slotPassphraseEntered(const QString & passphrase)242 KGpgTransactionPrivate::slotPassphraseEntered(const QString &passphrase)
243 {
244 	// not calling KGpgTransactionPrivate::write() here for obvious privacy reasons
245 	m_process->write(passphrase.toUtf8() + '\n');
246 	if (sender() == m_newPasswordDialog) {
247 		m_newPasswordDialog->deleteLater();
248 		m_newPasswordDialog = nullptr;
249 		m_parent->newPassphraseEntered();
250 	} else {
251 		Q_ASSERT(sender() == m_passwordDialog);
252 	}
253 }
254 
255 void
slotPassphraseAborted()256 KGpgTransactionPrivate::slotPassphraseAborted()
257 {
258 	Q_ASSERT((sender() == m_passwordDialog) ^ (sender() == m_newPasswordDialog));
259 	sender()->deleteLater();
260 	m_newPasswordDialog = nullptr;
261 	m_passwordDialog = nullptr;
262 	handlePassphraseAborted();
263 }
264 
265 void
handlePassphraseAborted()266 KGpgTransactionPrivate::handlePassphraseAborted()
267 {
268 	// sending "quit" here is useless as it would be interpreted as the passphrase
269 	m_process->closeWriteChannel();
270 	m_success = KGpgTransaction::TS_USER_ABORTED;
271 }
272 
273 void
write(const QByteArray & a)274 KGpgTransactionPrivate::write(const QByteArray &a)
275 {
276 	m_process->write(a);
277 #ifdef KGPG_DEBUG_TRANSACTIONS
278 	qCDebug(KGPG_LOG_TRANSACTIONS) << m_parent << a;
279 #endif /* KGPG_DEBUG_TRANSACTIONS */
280 }
281 
282 const QStringList &
hintNames(void)283 KGpgTransactionPrivate::hintNames (void)
284 {
285 	static QStringList hints;
286 
287 	if (hints.isEmpty()) {
288 		hints.insert(KGpgTransaction::HT_KEYEXPIRED,
289 				QLatin1String("[GNUPG:] KEYEXPIRED"));
290 		hints.insert(KGpgTransaction::HT_SIGEXPIRED,
291 				QLatin1String("[GNUPG:] SIGEXPIRED"));
292 		hints.insert(KGpgTransaction::HT_NOSECKEY,
293 				QLatin1String("[GNUPG:] NO_SECKEY"));
294 		hints.insert(KGpgTransaction::HT_ENCTO,
295 				QLatin1String("[GNUPG:] ENC_TO"));
296 		hints.insert(KGpgTransaction::HT_PINENTRY_LAUNCHED,
297 				QLatin1String("[GNUPG:] PINENTRY_LAUNCHED"));
298 	}
299 
300 	return hints;
301 }
302 
303 void
processDone()304 KGpgTransactionPrivate::processDone()
305 {
306 	m_parent->finish();
307 	Q_EMIT m_parent->infoProgress(100, 100);
308 	Q_EMIT m_parent->done(m_success);
309 #ifdef KGPG_DEBUG_TRANSACTIONS
310 	qCDebug(KGPG_LOG_TRANSACTIONS) << this << "result:" << m_success;
311 #endif /* KGPG_DEBUG_TRANSACTIONS */
312 }
313 
keyConsidered(const QString & line)314 bool KGpgTransactionPrivate::keyConsidered(const QString& line)
315 {
316 	if (!line.startsWith(QLatin1String("[GNUPG:] KEY_CONSIDERED ")))
317 		return false;
318 
319 	const QStringList &parts = line.split(QLatin1Char(' '), Qt::SkipEmptyParts);
320 	if (parts.count() < 3)
321 		m_parent->setSuccess(KGpgTransaction::TS_MSG_SEQUENCE);
322 	else if (!m_expectedFingerprints.isEmpty() &&
323 			!m_expectedFingerprints.contains(parts[2], Qt::CaseInsensitive))
324 		m_parent->setSuccess(KGpgTransaction::TS_MSG_SEQUENCE);
325 
326 	return true;
327 }
328