QtPass Developer documentation
QtPass test coverage
Current view: top level - src/sshauthsock.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 88.1 % 42 37
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 4 4

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief Implements SshAuthSock; a probed socket is adopted only after
       6              :  * `ssh-add -l` answers on it (isSshAgentReachable()).
       7              :  */
       8              : #include "sshauthsock.h"
       9              : #include "executor.h"
      10              : #include <QFileInfo>
      11              : #include <QProcessEnvironment>
      12              : #ifndef Q_OS_WIN
      13              : #include <sys/stat.h>
      14              : #endif
      15              : 
      16              : #include "qtpasslogging.h"
      17              : 
      18              : namespace {
      19              : /**
      20              :  * @brief Verify that a candidate SSH_AUTH_SOCK socket has a live agent
      21              :  *        listening, before letting QtPass switch the environment to it.
      22              :  *
      23              :  * Why: a user may have a working external SSH agent (OpenSSH ssh-agent,
      24              :  * KeePassXC / 1Password / yubikey-agent / gnome-keyring) configured in
      25              :  * their shell, but its env didn't propagate to the GUI launcher. If we
      26              :  * blindly adopt whatever `gpgconf --list-dirs agent-ssh-socket` reports —
      27              :  * even when gpg-agent isn't running with SSH support, or has no keys —
      28              :  * we'd be silently switching them away from their real agent.
      29              :  *
      30              :  * Approach: spawn `ssh-add -l` with SSH_AUTH_SOCK pointed at the candidate
      31              :  * (via Executor's env-aware overload, so the parent env isn't disturbed).
      32              :  * Treat exit codes 0 (keys present) and 1 (agent alive but key list empty —
      33              :  * legitimate for YubiKey-backed setups that enumerate on tap) as
      34              :  * "reachable". Anything else, including ssh-add not being on PATH, means
      35              :  * we don't adopt the candidate.
      36              :  *
      37              :  * @param candidate Path to validate.
      38              :  * @return true if the socket has a reachable agent; false otherwise.
      39              :  */
      40            2 : auto isSshAgentReachable(const QString &candidate) -> bool {
      41            2 :   if (candidate.isEmpty()) {
      42              :     return false;
      43              :   }
      44              :   // Build a minimal env that overrides SSH_AUTH_SOCK without polluting the
      45              :   // parent process. systemEnvironment() captures whatever was set when
      46              :   // QtPass started, then we override the one var of interest.
      47            2 :   QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
      48            4 :   env.insert(QStringLiteral("SSH_AUTH_SOCK"), candidate);
      49            2 :   QString out;
      50            2 :   QString err;
      51            6 :   const int exitCode = Executor::executeBlocking(
      52            4 :       env, QStringLiteral("ssh-add"), {QStringLiteral("-l")}, &out, &err);
      53              :   // OpenSSH ssh-add(1) exit codes: 0 = success / has keys, 1 = no
      54              :   // identities present, 2 = couldn't open a connection. Anything else is
      55              :   // also treated as unreachable (e.g. ssh-add not on PATH).
      56            2 :   return exitCode == 0 || exitCode == 1;
      57            2 : }
      58              : /// Ask @p program for a socket and adopt it when an agent answers there;
      59              : /// @p source names it in the log. Whether a socket was adopted.
      60            2 : auto adoptSocketFrom(const QString &program, const QStringList &args,
      61              :                      const char *source) -> bool {
      62            2 :   QString out;
      63            2 :   QString err;
      64            2 :   if (Executor::executeBlocking(program, args, &out, &err) != 0) {
      65              :     return false;
      66              :   }
      67              :   const QString socket = out.trimmed();
      68            2 :   if (socket.isEmpty()) {
      69              :     return false;
      70              :   }
      71            2 :   if (!isSshAgentReachable(socket)) {
      72            0 :     qCDebug(lcQtPass) << "SshAuthSock::initialise():" << source << "reported"
      73            0 :                       << socket << "but ssh-add -l rejected it; not adopting";
      74            0 :     return false;
      75              :   }
      76            2 :   qputenv("SSH_AUTH_SOCK", socket.toUtf8());
      77            2 :   qCDebug(lcQtPass) << "SshAuthSock::initialise(): set from" << source << ":"
      78            0 :                     << socket;
      79            2 :   return true;
      80              : }
      81              : 
      82              : } // namespace
      83              : 
      84            7 : auto SshAuthSock::overrideStatus(const QString &path) -> OverrideStatus {
      85            7 :   const QFileInfo fi(path);
      86            7 :   if (!fi.exists()) {
      87              :     return OverrideStatus::DoesNotExist;
      88              :   }
      89            5 :   if (!fi.isReadable()) {
      90              :     return OverrideStatus::NotReadable;
      91              :   }
      92              : #ifdef Q_OS_UNIX
      93              :   // S_ISSOCK isn't exposed by QFileInfo; go through stat(2). On Windows
      94              :   // ssh-agent uses a named pipe (\\.\pipe\openssh-ssh-agent.<sid>) and the
      95              :   // socket check would always fail, so skip it there — exists + readable is
      96              :   // the best we can do.
      97            3 :   struct stat st{};
      98            6 :   if (::stat(path.toLocal8Bit().constData(), &st) != 0 ||
      99            3 :       !S_ISSOCK(st.st_mode)) {
     100            2 :     return OverrideStatus::NotUnixDomainSocket;
     101              :   }
     102              : #endif
     103              :   return OverrideStatus::Valid;
     104            7 : }
     105              : 
     106              : // The rules are in the header. All probes go through
     107              : // Executor::executeBlocking with no timeout or cancel flag, so each one
     108              : // waits for its child (gpgconf, ssh-add -l) however long that takes. Any
     109              : // failure is silently absorbed (the user just doesn't get the auto-fix).
     110            5 : void SshAuthSock::initialise(const QString &override) {
     111              :   // Honour any value already in the environment — terminal launches,
     112              :   // explicit `.desktop` Exec= overrides, and parent-process exports must win.
     113           10 :   if (!qgetenv("SSH_AUTH_SOCK").isEmpty()) {
     114              :     return;
     115              :   }
     116              : 
     117              :   // Manual override from settings takes precedence over auto-probe.
     118            4 :   if (!override.isEmpty()) {
     119            2 :     qputenv("SSH_AUTH_SOCK", override.toUtf8());
     120            2 :     qCDebug(lcQtPass)
     121            0 :         << "SshAuthSock::initialise(): set from settings override:" << override;
     122            2 :     return;
     123              :   }
     124              : 
     125              :   // Auto-probe via gpgconf (canonical for gpg-agent's SSH support).
     126            8 :   if (adoptSocketFrom(
     127            2 :           QStringLiteral("gpgconf"),
     128            2 :           {QStringLiteral("--list-dirs"), QStringLiteral("agent-ssh-socket")},
     129              :           "gpgconf")) {
     130              :     return;
     131              :   }
     132              : #ifdef Q_OS_MACOS
     133              :   // On macOS, GUI-launched apps may have SSH_AUTH_SOCK in launchd's
     134              :   // per-session environment but not in the inherited process env.
     135              :   adoptSocketFrom(QStringLiteral("launchctl"),
     136              :                   {QStringLiteral("getenv"), QStringLiteral("SSH_AUTH_SOCK")},
     137              :                   "launchctl");
     138              : #endif
     139              : }
        

Generated by: LCOV version 2.0-1