]> git.ipfire.org Git - thirdparty/openssh-portable.git/commitdiff
Add gnome-ssh-askpass4 for GNOME 40+
authorJan Tojnar <jtojnar@gmail.com>
Thu, 18 May 2023 14:30:35 +0000 (16:30 +0200)
committerDamien Miller <djm@mindrot.org>
Mon, 14 Jul 2025 22:29:15 +0000 (15:29 -0700)
GTK 3 has been in maintenance mode for a while now, and it is on the road
to being abandoned. As a result, the dialogue looks out of place on modern
systems.

We could port it to GTK 4 but without the program being registered as an
application (i.e. having a .desktop file), GNOME Shell would ask for
permission to grab input every time.

Let’s instead use the GNOME Shell’s native prompt through the unstable
Gcr API.

contrib/Makefile
contrib/README
contrib/gnome-ssh-askpass4.c [new file with mode: 0644]

index 45d878bdcf22135ddcf5778460740e75dc9b8b83..1482783a8097f909841910d50fbdb325d973f09d 100644 (file)
@@ -1,7 +1,7 @@
 PKG_CONFIG = pkg-config
 
 all:
-       @echo "Valid targets: gnome-ssh-askpass1 gnome-ssh-askpass2 gnome-ssk-askpass3"
+       @echo "Valid targets: gnome-ssh-askpass1 gnome-ssh-askpass2 gnome-ssk-askpass3 gnome-ssh-askpass4"
 
 gnome-ssh-askpass1: gnome-ssh-askpass1.c
        $(CC) $(CFLAGS) `gnome-config --cflags gnome gnomeui` \
@@ -18,5 +18,10 @@ gnome-ssh-askpass3: gnome-ssh-askpass3.c
                gnome-ssh-askpass3.c -o gnome-ssh-askpass3 \
                `$(PKG_CONFIG) --libs gtk+-3.0 x11`
 
+gnome-ssh-askpass4: gnome-ssh-askpass4.c
+       $(CC) $(CFLAGS) `$(PKG_CONFIG) --cflags gcr-4 gio-2.0` \
+               gnome-ssh-askpass4.c -o gnome-ssh-askpass4 \
+               `$(PKG_CONFIG) --libs gcr-4 gio-2.0`
+
 clean:
        rm -f *.o gnome-ssh-askpass gnome-ssh-askpass[123]
index 60e19ba9faa834bf2ebdeafd04e01effb6ee0a24..614152a123aa57499133555b57665c18e1a5daa1 100644 (file)
@@ -30,10 +30,12 @@ ssh-copy-id:
 Phil Hands' <phil@hands.com> shell script to automate the process of adding
 your public key to a remote machine's ~/.ssh/authorized_keys file.
 
-gnome-ssh-askpass[12]:
+gnome-ssh-askpass[1234]:
 
-A GNOME and Gtk2 passphrase requesters. Use "make gnome-ssh-askpass1" or
-"make gnome-ssh-askpass2" to build.
+Graphical passhrase requesters. Use "make gnome-ssh-askpass1" to build
+a variant for ancient GNOME desktop, "make gnome-ssh-askpass2"
+for a GTK 2-based one, "make gnome-ssh-askpass3" for a GTK 3-based one,
+or "make gnome-ssh-askpass4" for a version for modern GNOME.
 
 sshd.pam.generic:
 
diff --git a/contrib/gnome-ssh-askpass4.c b/contrib/gnome-ssh-askpass4.c
new file mode 100644 (file)
index 0000000..182bce0
--- /dev/null
@@ -0,0 +1,249 @@
+/*
+ * Copyright (c) 2000-2002 Damien Miller.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/* GCR support by Jan Tojnar <jtojnar@gmail.com> */
+
+/*
+ * This is a simple SSH passphrase grabber for GNOME. To use it, set the
+ * environment variable SSH_ASKPASS to point to the location of
+ * gnome-ssh-askpass before calling "ssh-add < /dev/null".
+ */
+
+/*
+ * Known problems:
+ *   - This depends on unstable libgcr features
+ *   - long key fingerprints may be truncted:
+ *     https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/6781
+ */
+
+/*
+ * Compile with:
+ *
+ * cc -Wall `pkg-config --cflags gcr-4 gio-2.0` \
+ *    gnome-ssh-askpass4.c -o gnome-ssh-askpass \
+ *    `pkg-config --libs gcr-4 gio-2.0`
+ *
+ */
+
+#include <stdio.h>
+#include <err.h>
+
+#include <gio/gio.h>
+
+#define GCR_API_SUBJECT_TO_CHANGE 1
+#include <gcr/gcr.h>
+
+typedef enum _PromptType {
+       PROMPT_ENTRY,
+       PROMPT_CONFIRM,
+       PROMPT_NONE,
+} PromptType;
+
+typedef struct _PromptState {
+       GApplication *app;
+       char* message;
+       PromptType type;
+       int exit_status;
+} PromptState;
+
+static PromptState *
+prompt_state_new(GApplication *app, char* message, PromptType type)
+{
+       PromptState *state = g_malloc(sizeof(PromptState));
+       state->app = g_object_ref(app);
+       state->message = g_strdup(message);
+       state->type = type;
+       state->exit_status = -1;
+       return state;
+}
+
+static void
+prompt_state_free(PromptState *state)
+{
+       g_clear_object(&state->app);
+       g_free(state->message);
+       g_free(state);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(PromptState, prompt_state_free)
+
+static void
+prompt_password_done(GObject *source_object, GAsyncResult *res,
+    gpointer user_data)
+{
+       GcrPrompt *prompt = GCR_PROMPT(source_object);
+       PromptState *state = user_data;
+       g_autoptr(GError) error = NULL;
+
+       /*
+        * “The returned password is valid until the next time a method
+        * is called to display another prompt.”
+        */
+       const char *pw = gcr_prompt_password_finish(prompt, res, &error);
+
+       if ((!pw && !error) || (error && error->code == G_IO_ERROR_CANCELLED)) {
+               /* Operation was cancelled or timed out. */
+               state->exit_status = -1;
+       } else if (error) {
+               warnx("Failed to prompt for ssh-askpass: %s", error->message);
+               state->exit_status = 1;
+       } else {
+               /* Report passphrase if user selected Continue. */
+               g_autofree char *local = g_locale_from_utf8(pw, strlen(pw),
+                   NULL, NULL, NULL);
+
+               if (local != NULL) {
+                       puts(local);
+                       memset(local, '\0', strlen(local));
+               } else {
+                       puts(pw);
+               }
+               state->exit_status = 0;
+       }
+
+       g_application_release(state->app);
+}
+
+static void
+prompt_confirm_done(GObject *source_object, GAsyncResult *res,
+    gpointer user_data)
+{
+       GcrPrompt *prompt = GCR_PROMPT(source_object);
+       PromptState *state = user_data;
+       g_autoptr(GError) error = NULL;
+
+       GcrPromptReply reply = gcr_prompt_confirm_finish(prompt, res, &error);
+       if (error) {
+               if (error->code == G_IO_ERROR_CANCELLED) {
+                       /* Operation was cancelled or timed out. */
+                       state->exit_status = -1;
+               } else {
+                       state->exit_status = 1;
+                       warnx("Failed to prompt for ssh-askpass: %s",
+                           error->message);
+               }
+       } else if (reply == GCR_PROMPT_REPLY_CONTINUE ||
+           state->type == PROMPT_NONE) {
+               /*
+                * Since Gcr doesn’t yet support one button message
+                * boxes treat Cancel the same as Continue.
+                */
+               state->exit_status = 0;
+       } else {
+               /* GCR_PROMPT_REPLY_CANCEL */
+               state->exit_status = -1;
+       }
+
+       g_application_release(state->app);
+}
+
+static int
+command_line(GApplication* app, G_GNUC_UNUSED GApplicationCommandLine *cmdline,
+    gpointer user_data)
+{
+       PromptState *state = user_data;
+
+       /* Prevent app from exiting while waiting for the async callback. */
+       g_application_hold(app);
+
+       /* Wait indefinitely. */
+       int timeout_seconds = -1;
+       g_autoptr(GError) error = NULL;
+       GcrPrompt* prompt = gcr_system_prompt_open(timeout_seconds, NULL, &error);
+
+       if (!prompt) {
+               if (error->code == GCR_SYSTEM_PROMPT_IN_PROGRESS) {
+                       /*
+                        * This means the timeout elapsed, but no prompt
+                        * was ever shown.
+                        */
+                       warnx("Timeout: the Gcr system prompter was "
+                           "already in use.");
+               } else {
+                       warnx("Couldn’t create prompt for ssh-askpass: %s",
+                           error->message);
+               }
+
+               return 1;
+       }
+
+       gcr_prompt_set_message(prompt, "OpenSSH");
+       gcr_prompt_set_description(prompt, state->message);
+
+       /*
+        * XXX: Remove the Cancel button for PROMPT_NONE when GCR
+        * supports that.
+        */
+       if (state->type == PROMPT_ENTRY) {
+               gcr_prompt_password_async(prompt, NULL, prompt_password_done, state);
+       } else {
+               gcr_prompt_confirm_async(prompt, NULL, prompt_confirm_done, state);
+       }
+
+       /* The exit status will be changed in the async callbacks. */
+       return 1;
+}
+
+int
+main(int argc, char **argv)
+{
+       g_autoptr(GApplication) app = g_application_new(
+           "com.openssh.gnome-ssh-askpass4",
+           G_APPLICATION_HANDLES_COMMAND_LINE);
+       g_autofree char *message = NULL;
+
+       if (argc > 1) {
+               message = g_strjoinv(" ", argv + 1);
+       } else {
+               message = g_strdup("Enter your OpenSSH passphrase:");
+       }
+
+       const char *prompt_mode = getenv("SSH_ASKPASS_PROMPT");
+       PromptType type = PROMPT_ENTRY;
+       if (prompt_mode != NULL) {
+               if (strcasecmp(prompt_mode, "confirm") == 0) {
+                       type = PROMPT_CONFIRM;
+               } else if (strcasecmp(prompt_mode, "none") == 0) {
+                       type = PROMPT_NONE;
+               }
+       }
+
+       g_autoptr(PromptState) state = prompt_state_new(app, message, type);
+
+       g_signal_connect(app, "command-line", G_CALLBACK(command_line), state);
+
+       /*
+        * Since we are calling g_application_hold, we cannot use
+        * g_application_command_line_set_exit_status.
+        * To change the exit status returned by g_application_run:
+        *   “If the commandline invocation results in the mainloop running
+        *   (ie: because the use-count of the application increased to a
+        *   non-zero value) then the application is considered to have been
+        *   ‘successful’ in a certain sense, and the exit status is always
+        *   zero.”
+        */
+       (void)(g_application_run(app, argc, argv));
+
+       return state->exit_status;
+}