QtPass Developer documentation
QtPass test coverage
Current view: top level - src/importkeydialog.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 95.6 % 91 87
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 11 11

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief Implements ImportKeyDialog: the guarded key-file read, the blocking
       6              :  * `gpg --import` call and the status-line parser for the imported key id.
       7              :  */
       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.
      29              : /// @cond
      30              : // `[GNUPG:] IMPORT_OK <reason> <fingerprint>`: captures the full fingerprint,
      31              : // the preferred identifier.
      32              : static 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.
      36              : static 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.
      40              : static const QRegularExpression KEY_IMPORTED_FALLBACK(
      41              :     QStringLiteral(R"(gpg: key ([0-9A-Fa-f]{40}|[0-9A-Fa-f]{16}):.*imported)"));
      42              : /// @endcond
      43              : 
      44           18 : ImportKeyDialog::ImportKeyDialog(const QString &gpgExe, QWidget *parent)
      45           18 :     : QDialog(parent), ui(new Ui::ImportKeyDialog), m_gpgExe(gpgExe) {
      46           18 :   ui->setupUi(this);
      47           18 :   ui->importButton->setEnabled(false);
      48           18 : }
      49              : 
      50           36 : ImportKeyDialog::~ImportKeyDialog() = default;
      51              : 
      52            7 : auto ImportKeyDialog::importedKeyId() const -> QString {
      53            7 :   return m_importedKeyId;
      54              : }
      55              : 
      56            5 : void ImportKeyDialog::on_fileButton_clicked() {
      57              :   const QString fileName = QFileDialog::getOpenFileName(
      58           10 :       this, tr("Import GPG key"), QString(),
      59           20 :       tr("ASCII-armored GPG key") + " (*.asc);;" + tr("All files") + " (*)");
      60              : 
      61            5 :   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            4 :   QFile file;
      73            4 :   const QString target = QFileInfo(fileName).canonicalFilePath();
      74            4 :   if (target.isEmpty() || !Util::openRegularFile(target, file)) {
      75            4 :     PlainMessageBox::warning(this, tr("Import key"),
      76            4 :                              tr("Could not open file: %1").arg(fileName));
      77            2 :     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            2 :   const bool tooLarge = file.size() > kMaxKeyFileBytes;
      83              :   const QByteArray bytes =
      84            2 :       tooLarge ? QByteArray() : file.read(kMaxKeyFileBytes + 1);
      85            2 :   file.close();
      86            2 :   if (tooLarge || bytes.size() > kMaxKeyFileBytes) {
      87            0 :     PlainMessageBox::warning(
      88            0 :         this, tr("Import key"),
      89            0 :         tr("%1 is larger than a key file can be.").arg(fileName));
      90            0 :     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            2 :   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            2 :     QMessageBox::warning(
     100            1 :         this, tr("Import key"),
     101            1 :         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            2 :             .arg(fileName.toHtmlEscaped()));
     105            1 :     return;
     106              :   }
     107              : 
     108            2 :   ui->inputTextEdit->setPlainText(QString::fromUtf8(bytes));
     109            4 : }
     110              : 
     111            2 : void ImportKeyDialog::on_pasteButton_clicked() {
     112            2 :   const QClipboard *clipboard = QApplication::clipboard();
     113            2 :   const QString text = clipboard->text();
     114            2 :   if (text.isEmpty()) {
     115              :     // Don't wipe whatever the user already typed/loaded for an empty
     116              :     // clipboard.
     117              :     return;
     118              :   }
     119            1 :   ui->inputTextEdit->setPlainText(text);
     120              : }
     121              : 
     122            6 : void ImportKeyDialog::on_importButton_clicked() {
     123           12 :   const QString input = ui->inputTextEdit->toPlainText().trimmed();
     124            6 :   if (input.isEmpty()) {
     125              :     return;
     126              :   }
     127              : 
     128            5 :   if (importFromString(input)) {
     129            3 :     accept();
     130              :   }
     131              : }
     132              : 
     133           13 : void ImportKeyDialog::on_inputTextEdit_textChanged() {
     134           39 :   const bool hasInput = !ui->inputTextEdit->toPlainText().trimmed().isEmpty();
     135           13 :   ui->importButton->setEnabled(hasInput);
     136           13 : }
     137              : 
     138            5 : bool ImportKeyDialog::importFromString(const QString &input) {
     139            5 :   QString gpgExe = m_gpgExe.isEmpty() ? QStringLiteral("gpg") : m_gpgExe;
     140           55 :   QStringList args = {"--status-fd", "1", "--import", "--batch", "--yes"};
     141              : 
     142            5 :   QString stdOut;
     143            5 :   QString stdErr;
     144              : 
     145            5 :   int result = Executor::executeBlocking(gpgExe, args, input, &stdOut, &stdErr);
     146              : 
     147            5 :   if (result != 0) {
     148            2 :     showError(tr("GPG import failed:\n%1").arg(stdErr));
     149            1 :     return false;
     150              :   }
     151              : 
     152            4 :   QString keyId = parseGpgImportOutput(stdOut);
     153            4 :   if (keyId.isEmpty()) {
     154            4 :     keyId = parseGpgImportOutput(stdErr);
     155              :   }
     156              : 
     157            4 :   if (keyId.isEmpty()) {
     158            1 :     showError(tr("Could not parse imported key id from GPG output."));
     159            1 :     return false;
     160              :   }
     161              : 
     162            3 :   m_importedKeyId = keyId;
     163            3 :   showSuccess(keyId);
     164              :   return true;
     165              : }
     166              : 
     167           32 : auto ImportKeyDialog::parseGpgImportOutput(const QString &output) -> QString {
     168           32 :   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           54 :   for (const QString &line : lines) {
     177           35 :     const QRegularExpressionMatch match = IMPORT_OK_RE.match(line);
     178           35 :     if (match.hasMatch()) {
     179           13 :       return match.captured(1);
     180              :     }
     181           35 :   }
     182           35 :   for (const QString &line : lines) {
     183           18 :     const QRegularExpressionMatch match = IMPORTED_RE.match(line);
     184           18 :     if (match.hasMatch()) {
     185            2 :       return match.captured(1);
     186              :     }
     187           18 :   }
     188           27 :   for (const QString &line : lines) {
     189           16 :     const QRegularExpressionMatch match = KEY_IMPORTED_FALLBACK.match(line);
     190           16 :     if (match.hasMatch()) {
     191            6 :       return match.captured(1);
     192              :     }
     193           16 :   }
     194           11 :   return {};
     195              : }
     196              : 
     197            2 : void 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            2 :   PlainMessageBox::warning(this, tr("Import key"), message);
     201            2 : }
     202              : 
     203            3 : void ImportKeyDialog::showSuccess(const QString &keyId) {
     204            9 :   QMessageBox::information(this, tr("Import key"),
     205            3 :                            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            6 :                                .arg(keyId));
     210            3 : }
        

Generated by: LCOV version 2.0-1