QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
importkeydialog.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
8#include "importkeydialog.h"
9#include "plainmessagebox.h"
10#include "util.h"
11
12#include "executor.h"
13#include "ui_importkeydialog.h"
14
15#include <QApplication>
16#include <QClipboard>
17#include <QFile>
18#include <QFileDialog>
19#include <QFileInfo>
20#include <QMessageBox>
21#include <QPlainTextEdit>
22#include <QRegularExpression>
23#include <QString>
24
25// Locale-independent: gpg's machine-readable status output via --status-fd 1.
26// See doc/DETAILS in the GnuPG source for IMPORT_OK / IMPORTED grammar.
27// Doxygen reads `Type name(args);` as a function, so these three constants
28// are hidden from it and described here instead.
30// `[GNUPG:] IMPORT_OK <reason> <fingerprint>`: captures the full fingerprint,
31// the preferred identifier.
32static const QRegularExpression
33 IMPORT_OK_RE(QStringLiteral(R"(\‍[GNUPG:\‍] IMPORT_OK \d+ ([0-9A-Fa-f]+))"));
34// `[GNUPG:] IMPORTED <keyid> <userid>`: captures the 16-hex long key id, tried
35// after IMPORT_OK_RE.
36static const QRegularExpression
37 IMPORTED_RE(QStringLiteral(R"(\‍[GNUPG:\‍] IMPORTED ([0-9A-Fa-f]{16}))"));
38// Fallback for the human-readable (English-locale) `gpg: key <id>: ...
39// imported` line, last resort when no status line was seen.
40static const QRegularExpression KEY_IMPORTED_FALLBACK(
41 QStringLiteral(R"(gpg: key ([0-9A-Fa-f]{40}|[0-9A-Fa-f]{16}):.*imported)"));
43
44ImportKeyDialog::ImportKeyDialog(const QString &gpgExe, QWidget *parent)
45 : QDialog(parent), ui(new Ui::ImportKeyDialog), m_gpgExe(gpgExe) {
46 ui->setupUi(this);
47 ui->importButton->setEnabled(false);
48}
49
51
52auto ImportKeyDialog::importedKeyId() const -> QString {
53 return m_importedKeyId;
54}
55
57 const QString fileName = QFileDialog::getOpenFileName(
58 this, tr("Import GPG key"), QString(),
59 tr("ASCII-armored GPG key") + " (*.asc);;" + tr("All files") + " (*)");
60
61 if (fileName.isEmpty()) {
62 return;
63 }
64
65 // A key file, so a regular one of sensible size: a FIFO would block the
66 // GUI and /dev/zero (or a sparse file) would be read without end.
67 constexpr qint64 kMaxKeyFileBytes = 16LL * 1024 * 1024;
68 // A link is followed, deliberately: the user picked this file, unlike a
69 // name found in the store. Its target is then opened as the regular file
70 // it is at the moment of the open, without blocking: a FIFO swapped in
71 // after the pick would otherwise hang the GUI waiting for a writer.
72 QFile file;
73 const QString target = QFileInfo(fileName).canonicalFilePath();
74 if (target.isEmpty() || !Util::openRegularFile(target, file)) {
75 PlainMessageBox::warning(this, tr("Import key"),
76 tr("Could not open file: %1").arg(fileName));
77 return;
78 }
79
80 // The size first, so a large file is not read at all; the read limit
81 // still guards one that grows meanwhile.
82 const bool tooLarge = file.size() > kMaxKeyFileBytes;
83 const QByteArray bytes =
84 tooLarge ? QByteArray() : file.read(kMaxKeyFileBytes + 1);
85 file.close();
86 if (tooLarge || bytes.size() > kMaxKeyFileBytes) {
88 this, tr("Import key"),
89 tr("%1 is larger than a key file can be.").arg(fileName));
90 return;
91 }
92
93 // Only ASCII-armored content is shown in the text edit; binary keyrings
94 // would lose bytes through UTF-8 conversion. Reject anything that doesn't
95 // start with the PGP armor header.
96 if (!bytes.trimmed().startsWith("-----BEGIN PGP")) {
97 // Message body is rich text (uses <code>/<b>); escape the path so any
98 // characters in it cannot reach the HTML subset Qt renders.
99 QMessageBox::warning(
100 this, tr("Import key"),
101 tr("%1 does not look like an ASCII-armored GPG key. Convert it with "
102 "<code>gpg --armor --export</code> first, or paste the armored "
103 "block via <b>From clipboard</b>.")
104 .arg(fileName.toHtmlEscaped()));
105 return;
106 }
107
108 ui->inputTextEdit->setPlainText(QString::fromUtf8(bytes));
109}
110
112 const QClipboard *clipboard = QApplication::clipboard();
113 const QString text = clipboard->text();
114 if (text.isEmpty()) {
115 // Don't wipe whatever the user already typed/loaded for an empty
116 // clipboard.
117 return;
118 }
119 ui->inputTextEdit->setPlainText(text);
120}
121
123 const QString input = ui->inputTextEdit->toPlainText().trimmed();
124 if (input.isEmpty()) {
125 return;
126 }
127
128 if (importFromString(input)) {
129 accept();
130 }
131}
132
134 const bool hasInput = !ui->inputTextEdit->toPlainText().trimmed().isEmpty();
135 ui->importButton->setEnabled(hasInput);
136}
137
138bool ImportKeyDialog::importFromString(const QString &input) {
139 QString gpgExe = m_gpgExe.isEmpty() ? QStringLiteral("gpg") : m_gpgExe;
140 QStringList args = {"--status-fd", "1", "--import", "--batch", "--yes"};
141
142 QString stdOut;
143 QString stdErr;
144
145 int result = Executor::executeBlocking(gpgExe, args, input, &stdOut, &stdErr);
146
147 if (result != 0) {
148 showError(tr("GPG import failed:\n%1").arg(stdErr));
149 return false;
150 }
151
152 QString keyId = parseGpgImportOutput(stdOut);
153 if (keyId.isEmpty()) {
154 keyId = parseGpgImportOutput(stdErr);
155 }
156
157 if (keyId.isEmpty()) {
158 showError(tr("Could not parse imported key id from GPG output."));
159 return false;
160 }
161
162 m_importedKeyId = keyId;
163 showSuccess(keyId);
164 return true;
165}
166
167auto ImportKeyDialog::parseGpgImportOutput(const QString &output) -> QString {
168 const QStringList lines = output.split('\n', Qt::SkipEmptyParts);
169 // The order here is the priority order: prefer the locale-independent
170 // status lines over the English human-readable line, and within the
171 // status lines prefer IMPORT_OK (full fingerprint) over IMPORTED
172 // (long key id). Each regex is scanned across every line before we
173 // fall through to the next regex; the per-line ordering of gpg's
174 // output (which emits IMPORTED before IMPORT_OK) must not pick the
175 // weaker identifier.
176 for (const QString &line : lines) {
177 const QRegularExpressionMatch match = IMPORT_OK_RE.match(line);
178 if (match.hasMatch()) {
179 return match.captured(1);
180 }
181 }
182 for (const QString &line : lines) {
183 const QRegularExpressionMatch match = IMPORTED_RE.match(line);
184 if (match.hasMatch()) {
185 return match.captured(1);
186 }
187 }
188 for (const QString &line : lines) {
189 const QRegularExpressionMatch match = KEY_IMPORTED_FALLBACK.match(line);
190 if (match.hasMatch()) {
191 return match.captured(1);
192 }
193 }
194 return {};
195}
196
197void ImportKeyDialog::showError(const QString &message) {
198 // gpg's stderr quotes user IDs ("Name <a@b>"): plain, so they show as
199 // written and cannot be markup.
200 PlainMessageBox::warning(this, tr("Import key"), message);
201}
202
203void ImportKeyDialog::showSuccess(const QString &keyId) {
204 QMessageBox::information(this, tr("Import key"),
205 tr("Public key %1 imported. Check its fingerprint "
206 "with its owner before encrypting to it: "
207 "importing a key says nothing about who holds "
208 "it.")
209 .arg(keyId));
210}
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Run a command synchronously and return its exit code.
Definition executor.cpp:422
~ImportKeyDialog() override
Destroys the dialog and its generated UI.
static auto parseGpgImportOutput(const QString &output) -> QString
Parse GPG import output to extract a key id or fingerprint.
void showError(const QString &message)
Shows message in a plain-text warning box (gpg's stderr quotes user IDs, which must not be read as ma...
auto importedKeyId() const -> QString
Get the imported key id on success.
QString m_importedKeyId
Key id or fingerprint of the last successful import; empty until then. Read back through importedKeyI...
void on_fileButton_clicked()
"From file" button: picks a file, reads it as a regular file of at most 16 MiB, requires the -----BEG...
ImportKeyDialog(const QString &gpgExe, QWidget *parent=nullptr)
Construct an ImportKeyDialog.
QString m_gpgExe
Path of the gpg binary to run; empty means plain gpg on PATH.
void on_pasteButton_clicked()
"From clipboard" button: replaces the input field with the clipboard text, unless the clipboard is em...
void showSuccess(const QString &keyId)
Shows the "imported, now verify the fingerprint" information box.
QScopedPointer< Ui::ImportKeyDialog > ui
Generated form (importkeydialog.ui); owned.
auto importFromString(const QString &input) -> bool
Feeds input to gpg --status-fd 1 --import --batch --yes via Executor::executeBlocking() and records t...
void on_importButton_clicked()
"Import" button: runs importFromString() on the trimmed input and closes the dialog with accept() on ...
void on_inputTextEdit_textChanged()
Enables the Import button only while the input field holds non-blank text.
static auto openRegularFile(const QString &path, QFile &file) -> bool
Open path for reading as the regular file it is, not as what a link under that name points to: the ob...
Definition util.cpp:913
Executor, the FIFO queue every gpg, git and pass child process of QtPass goes through,...
Dialog that imports an ASCII-armored GPG public key from a file or the clipboard by running gpg --imp...
void warning(QWidget *parent, const QString &title, const QString &text)
Show a warning in plain text.
QMessageBox helpers that force Qt::PlainText so store-controlled names can never be read as rich-text...
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...