Files
openssh-portable/contrib/gnome-ssh-askpass4.c
Jan Tojnar e6805e2a6b Add gnome-ssh-askpass4 for GNOME 40+
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.
2025-07-14 15:29:15 -07:00

250 lines
6.9 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 doesnt 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("Couldnt 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;
}