1 /*
2 * Copyright (c) 2000-2002 Damien Miller. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
14 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
15 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
16 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
17 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
18 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
19 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
20 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
22 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 */
24
25 /* GCR support by Jan Tojnar <jtojnar@gmail.com> */
26
27 /*
28 * This is a simple SSH passphrase grabber for GNOME. To use it, set the
29 * environment variable SSH_ASKPASS to point to the location of
30 * gnome-ssh-askpass before calling "ssh-add < /dev/null".
31 */
32
33 /*
34 * Known problems:
35 * - This depends on unstable libgcr features
36 * - long key fingerprints may be truncted:
37 * https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/6781
38 */
39
40 /*
41 * Compile with:
42 *
43 * cc -Wall `pkg-config --cflags gcr-4 gio-2.0` \
44 * gnome-ssh-askpass4.c -o gnome-ssh-askpass \
45 * `pkg-config --libs gcr-4 gio-2.0`
46 *
47 */
48
49 #include <stdio.h>
50 #include <err.h>
51
52 #include <gio/gio.h>
53
54 #define GCR_API_SUBJECT_TO_CHANGE 1
55 #include <gcr/gcr.h>
56
57 typedef enum _PromptType {
58 PROMPT_ENTRY,
59 PROMPT_CONFIRM,
60 PROMPT_NONE,
61 } PromptType;
62
63 typedef struct _PromptState {
64 GApplication *app;
65 char* message;
66 PromptType type;
67 int exit_status;
68 } PromptState;
69
70 static PromptState *
prompt_state_new(GApplication * app,char * message,PromptType type)71 prompt_state_new(GApplication *app, char* message, PromptType type)
72 {
73 PromptState *state = g_malloc(sizeof(PromptState));
74 state->app = g_object_ref(app);
75 state->message = g_strdup(message);
76 state->type = type;
77 state->exit_status = -1;
78 return state;
79 }
80
81 static void
prompt_state_free(PromptState * state)82 prompt_state_free(PromptState *state)
83 {
84 g_clear_object(&state->app);
85 g_free(state->message);
86 g_free(state);
87 }
88
G_DEFINE_AUTOPTR_CLEANUP_FUNC(PromptState,prompt_state_free)89 G_DEFINE_AUTOPTR_CLEANUP_FUNC(PromptState, prompt_state_free)
90
91 static void
92 prompt_password_done(GObject *source_object, GAsyncResult *res,
93 gpointer user_data)
94 {
95 GcrPrompt *prompt = GCR_PROMPT(source_object);
96 PromptState *state = user_data;
97 g_autoptr(GError) error = NULL;
98
99 /*
100 * “The returned password is valid until the next time a method
101 * is called to display another prompt.”
102 */
103 const char *pw = gcr_prompt_password_finish(prompt, res, &error);
104
105 if ((!pw && !error) || (error && error->code == G_IO_ERROR_CANCELLED)) {
106 /* Operation was cancelled or timed out. */
107 state->exit_status = -1;
108 } else if (error) {
109 warnx("Failed to prompt for ssh-askpass: %s", error->message);
110 state->exit_status = 1;
111 } else {
112 /* Report passphrase if user selected Continue. */
113 g_autofree char *local = g_locale_from_utf8(pw, strlen(pw),
114 NULL, NULL, NULL);
115
116 if (local != NULL) {
117 puts(local);
118 memset(local, '\0', strlen(local));
119 } else {
120 puts(pw);
121 }
122 state->exit_status = 0;
123 }
124
125 g_application_release(state->app);
126 }
127
128 static void
prompt_confirm_done(GObject * source_object,GAsyncResult * res,gpointer user_data)129 prompt_confirm_done(GObject *source_object, GAsyncResult *res,
130 gpointer user_data)
131 {
132 GcrPrompt *prompt = GCR_PROMPT(source_object);
133 PromptState *state = user_data;
134 g_autoptr(GError) error = NULL;
135
136 GcrPromptReply reply = gcr_prompt_confirm_finish(prompt, res, &error);
137 if (error) {
138 if (error->code == G_IO_ERROR_CANCELLED) {
139 /* Operation was cancelled or timed out. */
140 state->exit_status = -1;
141 } else {
142 state->exit_status = 1;
143 warnx("Failed to prompt for ssh-askpass: %s",
144 error->message);
145 }
146 } else if (reply == GCR_PROMPT_REPLY_CONTINUE ||
147 state->type == PROMPT_NONE) {
148 /*
149 * Since Gcr doesn’t yet support one button message
150 * boxes treat Cancel the same as Continue.
151 */
152 state->exit_status = 0;
153 } else {
154 /* GCR_PROMPT_REPLY_CANCEL */
155 state->exit_status = -1;
156 }
157
158 g_application_release(state->app);
159 }
160
161 static int
command_line(GApplication * app,G_GNUC_UNUSED GApplicationCommandLine * cmdline,gpointer user_data)162 command_line(GApplication* app, G_GNUC_UNUSED GApplicationCommandLine *cmdline,
163 gpointer user_data)
164 {
165 PromptState *state = user_data;
166
167 /* Prevent app from exiting while waiting for the async callback. */
168 g_application_hold(app);
169
170 /* Wait indefinitely. */
171 int timeout_seconds = -1;
172 g_autoptr(GError) error = NULL;
173 GcrPrompt* prompt = gcr_system_prompt_open(timeout_seconds, NULL, &error);
174
175 if (!prompt) {
176 if (error->code == GCR_SYSTEM_PROMPT_IN_PROGRESS) {
177 /*
178 * This means the timeout elapsed, but no prompt
179 * was ever shown.
180 */
181 warnx("Timeout: the Gcr system prompter was "
182 "already in use.");
183 } else {
184 warnx("Couldn’t create prompt for ssh-askpass: %s",
185 error->message);
186 }
187
188 return 1;
189 }
190
191 gcr_prompt_set_message(prompt, "OpenSSH");
192 gcr_prompt_set_description(prompt, state->message);
193
194 /*
195 * XXX: Remove the Cancel button for PROMPT_NONE when GCR
196 * supports that.
197 */
198 if (state->type == PROMPT_ENTRY) {
199 gcr_prompt_password_async(prompt, NULL, prompt_password_done, state);
200 } else {
201 gcr_prompt_confirm_async(prompt, NULL, prompt_confirm_done, state);
202 }
203
204 /* The exit status will be changed in the async callbacks. */
205 return 1;
206 }
207
208 int
main(int argc,char ** argv)209 main(int argc, char **argv)
210 {
211 g_autoptr(GApplication) app = g_application_new(
212 "com.openssh.gnome-ssh-askpass4",
213 G_APPLICATION_HANDLES_COMMAND_LINE);
214 g_autofree char *message = NULL;
215
216 if (argc > 1) {
217 message = g_strjoinv(" ", argv + 1);
218 } else {
219 message = g_strdup("Enter your OpenSSH passphrase:");
220 }
221
222 const char *prompt_mode = getenv("SSH_ASKPASS_PROMPT");
223 PromptType type = PROMPT_ENTRY;
224 if (prompt_mode != NULL) {
225 if (strcasecmp(prompt_mode, "confirm") == 0) {
226 type = PROMPT_CONFIRM;
227 } else if (strcasecmp(prompt_mode, "none") == 0) {
228 type = PROMPT_NONE;
229 }
230 }
231
232 g_autoptr(PromptState) state = prompt_state_new(app, message, type);
233
234 g_signal_connect(app, "command-line", G_CALLBACK(command_line), state);
235
236 /*
237 * Since we are calling g_application_hold, we cannot use
238 * g_application_command_line_set_exit_status.
239 * To change the exit status returned by g_application_run:
240 * “If the commandline invocation results in the mainloop running
241 * (ie: because the use-count of the application increased to a
242 * non-zero value) then the application is considered to have been
243 * ‘successful’ in a certain sense, and the exit status is always
244 * zero.”
245 */
246 (void)(g_application_run(app, argc, argv));
247
248 return state->exit_status;
249 }
250