forked from rra/pam-krb5
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathprompting.c
363 lines (337 loc) · 13 KB
/
prompting.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
/*
* Prompt users for information.
*
* Handles all interaction with the PAM conversation, either directly or
* indirectly through the Kerberos libraries.
*
* Copyright 2005, 2006, 2007, 2009, 2014, 2017 Russ Allbery <[email protected]>
* Copyright 2011, 2012
* The Board of Trustees of the Leland Stanford Junior University
* Copyright 2005 Andres Salomon <[email protected]>
* Copyright 1999, 2000 Frank Cusack <[email protected]>
*
* See LICENSE for licensing terms.
*/
#include <config.h>
#include <portable/krb5.h>
#include <portable/pam.h>
#include <portable/system.h>
#include <assert.h>
#include <errno.h>
#include <internal.h>
#include <pam-util/args.h>
#include <pam-util/logging.h>
/*
* Prompt for a password.
*
* This function handles prompting both for a password for regular
* authentication and for passwords when changing one's password. The default
* prompt is simply "Password:" for the former. For the latter, a string
* describing the type of password is passed in as prefix. In this case, the
* prompts is:
*
* <prefix> <banner> password:
*
* where <prefix> is the argument passed and <banner> is the value of
* args->banner (defaulting to "Kerberos").
*
* If args->config->expose_account is set, we append the principal name (taken
* from args->config->ctx->princ) before the colon, so the prompts are:
*
* Password for <principal>:
* <prefix> <banner> password for <principal>:
*
* Normally this is not done because it exposes the realm and possibly any
* username to principal mappings, plus may confuse some ssh clients if sshd
* passes the prompt back to the client.
*
* The entered password is stored in password. The memory is allocated by the
* application and returned as part of the PAM conversation. It must be freed
* by the caller.
*
* Returns a PAM success or error code.
*/
int
pamk5_get_password(struct pam_args *args, const char *prefix, char **password)
{
struct context *ctx = args->config->ctx;
char *prompt = NULL;
char *principal = NULL;
krb5_error_code k5_errno;
int retval;
if (args->config->expose_account || prefix != NULL)
if (ctx != NULL && ctx->context != NULL && ctx->princ != NULL) {
k5_errno = krb5_unparse_name(ctx->context, ctx->princ, &principal);
if (k5_errno != 0)
putil_debug_krb5(args, k5_errno, "krb5_unparse_name failed");
}
if (prefix == NULL) {
if (args->config->expose_account && principal != NULL) {
if (asprintf(&prompt, "Password for %s: ", principal) < 0)
goto fail;
} else {
prompt = strdup("Password: ");
if (prompt == NULL)
goto fail;
}
} else {
const char *banner;
const char *bspace;
banner = (args->config->banner == NULL) ? "" : args->config->banner;
bspace = (args->config->banner == NULL) ? "" : " ";
if (args->config->expose_account && principal != NULL) {
retval = asprintf(&prompt, "%s%s%s password for %s: ", prefix,
bspace, banner, principal);
if (retval < 0)
goto fail;
} else {
retval = asprintf(&prompt, "%s%s%s password: ", prefix, bspace,
banner);
if (retval < 0)
goto fail;
}
}
if (principal != NULL)
krb5_free_unparsed_name(ctx->context, principal);
retval = pamk5_conv(args, prompt, PAM_PROMPT_ECHO_OFF, password);
free(prompt);
return retval;
fail:
if (principal != NULL)
krb5_free_unparsed_name(ctx->context, principal);
return PAM_BUF_ERR;
}
/*
* Get information from the user or display a message to the user, as
* determined by type. If PAM_SILENT was given, don't pass any text or error
* messages to the application.
*
* The response variable is set to the response returned by the conversation
* function on a successful return if a response was desired. Caller is
* responsible for freeing it.
*/
int
pamk5_conv(struct pam_args *args, const char *message, int type,
char **response)
{
int pamret;
struct pam_message msg;
PAM_CONST struct pam_message *pmsg;
struct pam_response *resp = NULL;
struct pam_conv *conv;
int want_reply;
if (args->silent && (type == PAM_ERROR_MSG || type == PAM_TEXT_INFO))
return PAM_SUCCESS;
pamret = pam_get_item(args->pamh, PAM_CONV, (PAM_CONST void **) &conv);
if (pamret != PAM_SUCCESS)
return pamret;
if (conv->conv == NULL)
return PAM_CONV_ERR;
pmsg = &msg;
msg.msg_style = type;
msg.msg = (PAM_CONST char *) message;
pamret = conv->conv(1, &pmsg, &resp, conv->appdata_ptr);
if (pamret != PAM_SUCCESS)
return pamret;
/*
* Only expect a response for PAM_PROMPT_ECHO_OFF or PAM_PROMPT_ECHO_ON
* message types. This mildly annoying logic makes sure that everything
* is freed properly (except the response itself, if wanted, which is
* returned for the caller to free) and that the success status is set
* based on whether the reply matched our expectations.
*
* If we got a reply even though we didn't want one, still overwrite the
* reply before freeing in case it was a password.
*/
want_reply = (type == PAM_PROMPT_ECHO_OFF || type == PAM_PROMPT_ECHO_ON);
if (resp == NULL || resp->resp == NULL)
pamret = want_reply ? PAM_CONV_ERR : PAM_SUCCESS;
else if (want_reply && response != NULL) {
*response = resp->resp;
pamret = PAM_SUCCESS;
} else {
memset(resp->resp, 0, strlen(resp->resp));
free(resp->resp);
pamret = want_reply ? PAM_SUCCESS : PAM_CONV_ERR;
}
free(resp);
return pamret;
}
/*
* This is the generic prompting function called by both the MIT Kerberos and
* Heimdal prompting implementations. The MIT function takes a name and the
* Heimdal function doesn't, which is the only difference between the two.
* Both are simple wrappers that call this function.
*
* There are a lot of structures and different layers of code at work here,
* making this code quite confusing. This function is a prompter function to
* pass into the Kerberos library, in particular krb5_get_init_creds_password.
* It is used by the Kerberos library to prompt for a password if need be, and
* also to prompt for password changes if the password was expired.
*
* The purpose of this function is to serve as glue between the Kerberos
* library and the application (by way of the PAM glue). PAM expects us to
* pass back to the conversation function an array of prompts and receive from
* the application an array of responses to those prompts. We pass the
* application an array of struct pam_message pointers, and the application
* passes us an array of struct pam_response pointers.
*
* Kerberos, meanwhile, passes us in an array of krb5_prompt structs. This
* struct contains the prompt, a flag saying whether to suppress echoing of
* what the user types for that prompt, and a buffer into which to store the
* response.
*
* Therefore, what we're doing here is copying the prompts from the
* krb5_prompt structs into pam_message structs, calling the conversation
* function, and then copying the responses back out of pam_response structs
* into the krb5_prompt structs to return to the Kerberos library.
*/
krb5_error_code
pamk5_prompter_krb5(krb5_context context UNUSED, void *data, const char *name,
const char *banner, int num_prompts, krb5_prompt *prompts)
{
struct pam_args *args = data;
int total_prompts = num_prompts;
int pam_prompts, pamret, i;
int retval = KRB5KRB_ERR_GENERIC;
struct pam_message **msg;
struct pam_response *resp = NULL;
struct pam_conv *conv;
/* Treat the name and banner as prompts that doesn't need input. */
if (name != NULL && !args->silent)
total_prompts++;
if (banner != NULL && !args->silent)
total_prompts++;
/* If we have zero prompts, do nothing, silently. */
if (total_prompts == 0)
return 0;
/* Obtain the conversation function from the application. */
pamret = pam_get_item(args->pamh, PAM_CONV, (PAM_CONST void **) &conv);
if (pamret != 0)
return KRB5KRB_ERR_GENERIC;
if (conv->conv == NULL)
return KRB5KRB_ERR_GENERIC;
/*
* Allocate memory to copy all of the prompts into a pam_message.
*
* Linux PAM and Solaris PAM expect different things here. Solaris PAM
* expects to receive a pointer to a pointer to an array of pam_message
* structs. Linux PAM expects to receive a pointer to an array of
* pointers to pam_message structs. In order for the module to work with
* either PAM implementation, we need to set up a structure that is valid
* either way you look at it.
*
* We do this by making msg point to the array of struct pam_message
* pointers (what Linux PAM expects), and then make the first one of those
* pointers point to the array of pam_message structs. Solaris will then
* be happy, looking at only the first element of the outer array and
* finding it pointing to the inner array. Then, for Linux, we point the
* other elements of the outer array to the storage allocated in the inner
* array.
*
* All this also means we have to be careful how we free the resulting
* structure since it's double-linked in a subtle way. Thankfully, we get
* to free it ourselves.
*/
msg = calloc(total_prompts, sizeof(struct pam_message *));
if (msg == NULL)
return ENOMEM;
*msg = calloc(total_prompts, sizeof(struct pam_message));
if (*msg == NULL) {
free(msg);
return ENOMEM;
}
for (i = 1; i < total_prompts; i++)
msg[i] = msg[0] + i;
/* pam_prompts is an index into msg and a count when we're done. */
pam_prompts = 0;
if (name != NULL && !args->silent) {
msg[pam_prompts]->msg = strdup(name);
if (msg[pam_prompts]->msg == NULL)
goto cleanup;
msg[pam_prompts]->msg_style = PAM_TEXT_INFO;
pam_prompts++;
}
if (banner != NULL && !args->silent) {
assert(pam_prompts < total_prompts);
msg[pam_prompts]->msg = strdup(banner);
if (msg[pam_prompts]->msg == NULL)
goto cleanup;
msg[pam_prompts]->msg_style = PAM_TEXT_INFO;
pam_prompts++;
}
for (i = 0; i < num_prompts; i++) {
int status;
size_t len;
bool has_colon;
/*
* Heimdal adds the trailing colon and space, while MIT does not.
* Work around the difference by looking to see if there's a trailing
* colon and space already and only adding it if there is not.
*/
len = strlen(prompts[i].prompt);
has_colon = (len > 2
&& prompts[i].prompt[len - 1] == ' '
&& prompts[i].prompt[len - 2] == ':');
status = asprintf((char **) &msg[pam_prompts]->msg, "%s%s",
prompts[i].prompt, has_colon ? "" : ": ");
if (status < 0)
goto cleanup;
assert(pam_prompts < total_prompts);
msg[pam_prompts]->msg_style = prompts[i].hidden ? PAM_PROMPT_ECHO_OFF
: PAM_PROMPT_ECHO_ON;
pam_prompts++;
}
/* Call into the application conversation function. */
pamret = conv->conv(pam_prompts, (PAM_CONST struct pam_message **) msg,
&resp, conv->appdata_ptr);
if (pamret != 0)
goto cleanup;
if (resp == NULL)
goto cleanup;
/*
* Reuse pam_prompts as a starting index and copy the data into the reply
* area of the krb5_prompt structs.
*/
pam_prompts = 0;
if (name != NULL && !args->silent)
pam_prompts++;
if (banner != NULL && !args->silent)
pam_prompts++;
for (i = 0; i < num_prompts; i++, pam_prompts++) {
size_t len;
if (resp[pam_prompts].resp == NULL)
goto cleanup;
len = strlen(resp[pam_prompts].resp);
if (len > prompts[i].reply->length)
goto cleanup;
/*
* The trailing nul is not included in length, but other applications
* expect it to be there. Therefore, we copy one more byte than the
* actual length of the password, but set length to just the length of
* the password.
*/
memcpy(prompts[i].reply->data, resp[pam_prompts].resp, len + 1);
prompts[i].reply->length = (unsigned int) len;
}
retval = 0;
cleanup:
for (i = 0; i < total_prompts; i++)
free((char *) msg[i]->msg);
free(*msg);
free(msg);
/*
* Clean up the responses. These may contain passwords, so we overwrite
* them before we free them.
*/
if (resp != NULL) {
for (i = 0; i < total_prompts; i++) {
if (resp[i].resp != NULL) {
memset(resp[i].resp, 0, strlen(resp[i].resp));
free(resp[i].resp);
}
}
free(resp);
}
return retval;
}