xref: /freebsd/crypto/krb5/src/lib/krb5/krb/preauth_otp.c (revision f1c4c3daccbaf3820f0e2224de53df12fc952fcc)
1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /* lib/krb5/krb/preauth_otp.c - OTP clpreauth module */
3 /*
4  * Copyright 2011 NORDUnet A/S.  All rights reserved.
5  * Copyright 2011 Red Hat, Inc.  All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions are met:
9  *
10  *    1. Redistributions of source code must retain the above copyright
11  *       notice, this list of conditions and the following disclaimer.
12  *
13  *    2. Redistributions in binary form must reproduce the above copyright
14  *       notice, this list of conditions and the following disclaimer in
15  *       the documentation and/or other materials provided with the
16  *       distribution.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
21  * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
22  * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30 
31 #include "k5-int.h"
32 #include "k5-json.h"
33 #include "int-proto.h"
34 #include "os-proto.h"
35 
36 #include <krb5/clpreauth_plugin.h>
37 #include <ctype.h>
38 
39 static krb5_preauthtype otp_client_supported_pa_types[] =
40     { KRB5_PADATA_OTP_CHALLENGE, 0 };
41 
42 /* Frees a tokeninfo. */
43 static void
free_tokeninfo(krb5_responder_otp_tokeninfo * ti)44 free_tokeninfo(krb5_responder_otp_tokeninfo *ti)
45 {
46     if (ti == NULL)
47         return;
48 
49     free(ti->alg_id);
50     free(ti->challenge);
51     free(ti->token_id);
52     free(ti->vendor);
53     free(ti);
54 }
55 
56 /* Converts a property of a json object into a char*. */
57 static krb5_error_code
codec_value_to_string(k5_json_object obj,const char * key,char ** string)58 codec_value_to_string(k5_json_object obj, const char *key, char **string)
59 {
60     k5_json_value val;
61     char *str;
62 
63     val = k5_json_object_get(obj, key);
64     if (val == NULL)
65         return ENOENT;
66 
67     if (k5_json_get_tid(val) != K5_JSON_TID_STRING)
68         return EINVAL;
69 
70     str = strdup(k5_json_string_utf8(val));
71     if (str == NULL)
72         return ENOMEM;
73 
74     *string = str;
75     return 0;
76 }
77 
78 /* Converts a property of a json object into a krb5_data struct. */
79 static krb5_error_code
codec_value_to_data(k5_json_object obj,const char * key,krb5_data * data)80 codec_value_to_data(k5_json_object obj, const char *key, krb5_data *data)
81 {
82     krb5_error_code retval;
83     char *tmp;
84 
85     retval = codec_value_to_string(obj, key, &tmp);
86     if (retval != 0)
87         return retval;
88 
89     *data = string2data(tmp);
90     return 0;
91 }
92 
93 /* Converts a krb5_data struct into a property of a JSON object. */
94 static krb5_error_code
codec_data_to_value(krb5_data * data,k5_json_object obj,const char * key)95 codec_data_to_value(krb5_data *data, k5_json_object obj, const char *key)
96 {
97     krb5_error_code retval;
98     k5_json_string str;
99 
100     if (data->data == NULL)
101         return 0;
102 
103     retval = k5_json_string_create_len(data->data, data->length, &str);
104     if (retval)
105         return retval;
106 
107     retval = k5_json_object_set(obj, key, str);
108     k5_json_release(str);
109     return retval;
110 }
111 
112 /* Converts a property of a json object into a krb5_int32. */
113 static krb5_error_code
codec_value_to_int32(k5_json_object obj,const char * key,krb5_int32 * int32)114 codec_value_to_int32(k5_json_object obj, const char *key, krb5_int32 *int32)
115 {
116     k5_json_value val;
117 
118     val = k5_json_object_get(obj, key);
119     if (val == NULL)
120         return ENOENT;
121 
122     if (k5_json_get_tid(val) != K5_JSON_TID_NUMBER)
123         return EINVAL;
124 
125     *int32 = k5_json_number_value(val);
126     return 0;
127 }
128 
129 /* Converts a krb5_int32 into a property of a JSON object. */
130 static krb5_error_code
codec_int32_to_value(krb5_int32 int32,k5_json_object obj,const char * key)131 codec_int32_to_value(krb5_int32 int32, k5_json_object obj, const char *key)
132 {
133     krb5_error_code retval;
134     k5_json_number num;
135 
136     if (int32 == -1)
137         return 0;
138 
139     retval = k5_json_number_create(int32, &num);
140     if (retval)
141         return retval;
142 
143     retval = k5_json_object_set(obj, key, num);
144     k5_json_release(num);
145     return retval;
146 }
147 
148 /* Converts a krb5_otp_tokeninfo into a JSON object. */
149 static krb5_error_code
codec_encode_tokeninfo(krb5_otp_tokeninfo * ti,k5_json_object * out)150 codec_encode_tokeninfo(krb5_otp_tokeninfo *ti, k5_json_object *out)
151 {
152     krb5_error_code retval;
153     k5_json_object obj;
154     krb5_flags flags;
155 
156     retval = k5_json_object_create(&obj);
157     if (retval != 0)
158         goto error;
159 
160     flags = KRB5_RESPONDER_OTP_FLAGS_COLLECT_TOKEN;
161     if (ti->flags & KRB5_OTP_FLAG_COLLECT_PIN) {
162         flags |= KRB5_RESPONDER_OTP_FLAGS_COLLECT_PIN;
163         if (ti->flags & KRB5_OTP_FLAG_SEPARATE_PIN)
164             flags |= KRB5_RESPONDER_OTP_FLAGS_NEXTOTP;
165     }
166     if (ti->flags & KRB5_OTP_FLAG_NEXTOTP)
167         flags |= KRB5_RESPONDER_OTP_FLAGS_NEXTOTP;
168 
169     retval = codec_int32_to_value(flags, obj, "flags");
170     if (retval != 0)
171         goto error;
172 
173     retval = codec_data_to_value(&ti->vendor, obj, "vendor");
174     if (retval != 0)
175         goto error;
176 
177     retval = codec_data_to_value(&ti->challenge, obj, "challenge");
178     if (retval != 0)
179         goto error;
180 
181     retval = codec_int32_to_value(ti->length, obj, "length");
182     if (retval != 0)
183         goto error;
184 
185     if (ti->format != KRB5_OTP_FORMAT_BASE64 &&
186         ti->format != KRB5_OTP_FORMAT_BINARY) {
187         retval = codec_int32_to_value(ti->format, obj, "format");
188         if (retval != 0)
189             goto error;
190     }
191 
192     retval = codec_data_to_value(&ti->token_id, obj, "tokenID");
193     if (retval != 0)
194         goto error;
195 
196     retval = codec_data_to_value(&ti->alg_id, obj, "algID");
197     if (retval != 0)
198         goto error;
199 
200     *out = obj;
201     return 0;
202 
203 error:
204     k5_json_release(obj);
205     return retval;
206 }
207 
208 /* Converts a krb5_pa_otp_challenge into a JSON object. */
209 static krb5_error_code
codec_encode_challenge(krb5_context ctx,krb5_pa_otp_challenge * chl,char ** json)210 codec_encode_challenge(krb5_context ctx, krb5_pa_otp_challenge *chl,
211                        char **json)
212 {
213     k5_json_object obj = NULL, tmp = NULL;
214     k5_json_string str = NULL;
215     k5_json_array arr = NULL;
216     krb5_error_code retval;
217     size_t i;
218 
219     retval = k5_json_object_create(&obj);
220     if (retval != 0)
221         goto cleanup;
222 
223     if (chl->service.data) {
224         retval = k5_json_string_create_len(chl->service.data,
225                                            chl->service.length, &str);
226         if (retval != 0)
227             goto cleanup;
228         retval = k5_json_object_set(obj, "service", str);
229         k5_json_release(str);
230         if (retval != 0)
231             goto cleanup;
232     }
233 
234     retval = k5_json_array_create(&arr);
235     if (retval != 0)
236         goto cleanup;
237 
238     for (i = 0; chl->tokeninfo[i] != NULL ; i++) {
239         retval = codec_encode_tokeninfo(chl->tokeninfo[i], &tmp);
240         if (retval != 0)
241             goto cleanup;
242 
243         retval = k5_json_array_add(arr, tmp);
244         k5_json_release(tmp);
245         if (retval != 0)
246             goto cleanup;
247     }
248 
249     retval = k5_json_object_set(obj, "tokenInfo", arr);
250     if (retval != 0)
251         goto cleanup;
252 
253     retval = k5_json_encode(obj, json);
254     if (retval)
255         goto cleanup;
256 
257 cleanup:
258     k5_json_release(arr);
259     k5_json_release(obj);
260     return retval;
261 }
262 
263 /* Converts a JSON object into a krb5_responder_otp_tokeninfo. */
264 static krb5_responder_otp_tokeninfo *
codec_decode_tokeninfo(k5_json_object obj)265 codec_decode_tokeninfo(k5_json_object obj)
266 {
267     krb5_responder_otp_tokeninfo *ti = NULL;
268     krb5_error_code retval;
269 
270     ti = calloc(1, sizeof(krb5_responder_otp_tokeninfo));
271     if (ti == NULL)
272         goto error;
273 
274     retval = codec_value_to_int32(obj, "flags", &ti->flags);
275     if (retval != 0)
276         goto error;
277 
278     retval = codec_value_to_string(obj, "vendor", &ti->vendor);
279     if (retval != 0 && retval != ENOENT)
280         goto error;
281 
282     retval = codec_value_to_string(obj, "challenge", &ti->challenge);
283     if (retval != 0 && retval != ENOENT)
284         goto error;
285 
286     retval = codec_value_to_int32(obj, "length", &ti->length);
287     if (retval == ENOENT)
288         ti->length = -1;
289     else if (retval != 0)
290         goto error;
291 
292     retval = codec_value_to_int32(obj, "format", &ti->format);
293     if (retval == ENOENT)
294         ti->format = -1;
295     else if (retval != 0)
296         goto error;
297 
298     retval = codec_value_to_string(obj, "tokenID", &ti->token_id);
299     if (retval != 0 && retval != ENOENT)
300         goto error;
301 
302     retval = codec_value_to_string(obj, "algID", &ti->alg_id);
303     if (retval != 0 && retval != ENOENT)
304         goto error;
305 
306     return ti;
307 
308 error:
309     free_tokeninfo(ti);
310     return NULL;
311 }
312 
313 /* Converts a JSON object into a krb5_responder_otp_challenge. */
314 static krb5_responder_otp_challenge *
codec_decode_challenge(krb5_context ctx,const char * json)315 codec_decode_challenge(krb5_context ctx, const char *json)
316 {
317     krb5_responder_otp_challenge *chl = NULL;
318     k5_json_value obj = NULL, arr = NULL, tmp = NULL;
319     krb5_error_code retval;
320     size_t i;
321 
322     retval = k5_json_decode(json, &obj);
323     if (retval != 0)
324         goto error;
325 
326     if (k5_json_get_tid(obj) != K5_JSON_TID_OBJECT)
327         goto error;
328 
329     arr = k5_json_object_get(obj, "tokenInfo");
330     if (arr == NULL)
331         goto error;
332 
333     if (k5_json_get_tid(arr) != K5_JSON_TID_ARRAY)
334         goto error;
335 
336     chl = calloc(1, sizeof(krb5_responder_otp_challenge));
337     if (chl == NULL)
338         goto error;
339 
340     chl->tokeninfo = calloc(k5_json_array_length(arr) + 1,
341                             sizeof(krb5_responder_otp_tokeninfo*));
342     if (chl->tokeninfo == NULL)
343         goto error;
344 
345     retval = codec_value_to_string(obj, "service", &chl->service);
346     if (retval != 0 && retval != ENOENT)
347         goto error;
348 
349     for (i = 0; i < k5_json_array_length(arr); i++) {
350         tmp = k5_json_array_get(arr, i);
351         if (k5_json_get_tid(tmp) != K5_JSON_TID_OBJECT)
352             goto error;
353 
354         chl->tokeninfo[i] = codec_decode_tokeninfo(tmp);
355         if (chl->tokeninfo[i] == NULL)
356             goto error;
357     }
358 
359     k5_json_release(obj);
360     return chl;
361 
362 error:
363     if (chl != NULL) {
364         for (i = 0; chl->tokeninfo != NULL && chl->tokeninfo[i] != NULL; i++)
365             free_tokeninfo(chl->tokeninfo[i]);
366         free(chl->tokeninfo);
367         free(chl);
368     }
369     k5_json_release(obj);
370     return NULL;
371 }
372 
373 /* Decode the responder answer into a tokeninfo, a value and a pin. */
374 static krb5_error_code
codec_decode_answer(krb5_context context,const char * answer,krb5_otp_tokeninfo ** tis,krb5_otp_tokeninfo ** ti,krb5_data * value,krb5_data * pin)375 codec_decode_answer(krb5_context context, const char *answer,
376                     krb5_otp_tokeninfo **tis, krb5_otp_tokeninfo **ti,
377                     krb5_data *value, krb5_data *pin)
378 {
379     krb5_error_code retval;
380     k5_json_value val = NULL;
381     krb5_int32 indx;
382     krb5_data tmp;
383     size_t i;
384 
385     if (answer == NULL)
386         return EBADMSG;
387 
388     retval = k5_json_decode(answer, &val);
389     if (retval != 0)
390         goto cleanup;
391 
392     if (k5_json_get_tid(val) != K5_JSON_TID_OBJECT)
393         goto cleanup;
394 
395     retval = codec_value_to_int32(val, "tokeninfo", &indx);
396     if (retval != 0)
397         goto cleanup;
398 
399     for (i = 0; tis[i] != NULL; i++) {
400         if (i == (size_t)indx) {
401             retval = codec_value_to_data(val, "value", &tmp);
402             if (retval != 0 && retval != ENOENT)
403                 goto cleanup;
404 
405             retval = codec_value_to_data(val, "pin", pin);
406             if (retval != 0 && retval != ENOENT) {
407                 krb5_free_data_contents(context, &tmp);
408                 goto cleanup;
409             }
410 
411             *value = tmp;
412             *ti = tis[i];
413             retval = 0;
414             goto cleanup;
415         }
416     }
417     retval = EINVAL;
418 
419 cleanup:
420     k5_json_release(val);
421     return retval;
422 }
423 
424 /* Takes the nonce from the challenge and encrypts it into the request. */
425 static krb5_error_code
encrypt_nonce(krb5_context ctx,krb5_keyblock * key,const krb5_pa_otp_challenge * chl,krb5_pa_otp_req * req)426 encrypt_nonce(krb5_context ctx, krb5_keyblock *key,
427               const krb5_pa_otp_challenge *chl, krb5_pa_otp_req *req)
428 {
429     krb5_error_code retval;
430     krb5_enc_data encdata;
431     krb5_data *er;
432 
433     /* Encode the nonce. */
434     retval = encode_krb5_pa_otp_enc_req(&chl->nonce, &er);
435     if (retval != 0)
436         return retval;
437 
438     /* Do the encryption. */
439     retval = krb5_encrypt_helper(ctx, key, KRB5_KEYUSAGE_PA_OTP_REQUEST,
440                                  er, &encdata);
441     krb5_free_data(ctx, er);
442     if (retval != 0)
443         return retval;
444 
445     req->enc_data = encdata;
446     return 0;
447 }
448 
449 /* Checks to see if the user-supplied otp value matches the length and format
450  * of the supplied tokeninfo. */
451 static int
otpvalue_matches_tokeninfo(const char * otpvalue,krb5_otp_tokeninfo * ti)452 otpvalue_matches_tokeninfo(const char *otpvalue, krb5_otp_tokeninfo *ti)
453 {
454     int (*table[])(int c) = { isdigit, isxdigit, isalnum };
455 
456     if (otpvalue == NULL || ti == NULL)
457         return 0;
458 
459     if (ti->length >= 0 && strlen(otpvalue) != (size_t)ti->length)
460         return 0;
461 
462     if (ti->format >= 0 && ti->format < 3) {
463         while (*otpvalue) {
464             if (!(*table[ti->format])((unsigned char)*otpvalue++))
465                 return 0;
466         }
467     }
468 
469     return 1;
470 }
471 
472 /* Performs a prompt and saves the response in the out parameter. */
473 static krb5_error_code
doprompt(krb5_context context,krb5_prompter_fct prompter,void * prompter_data,const char * banner,const char * prompttxt,char * out,size_t len)474 doprompt(krb5_context context, krb5_prompter_fct prompter, void *prompter_data,
475          const char *banner, const char *prompttxt, char *out, size_t len)
476 {
477     krb5_prompt prompt;
478     krb5_data prompt_reply;
479     krb5_error_code retval;
480     krb5_prompt_type prompt_type = KRB5_PROMPT_TYPE_PREAUTH;
481 
482     if (prompttxt == NULL || out == NULL)
483         return EINVAL;
484 
485     memset(out, 0, len);
486 
487     prompt_reply = make_data(out, len);
488     prompt.reply = &prompt_reply;
489     prompt.prompt = (char *)prompttxt;
490     prompt.hidden = 1;
491 
492     /* PROMPTER_INVOCATION */
493     k5_set_prompt_types(context, &prompt_type);
494     retval = (*prompter)(context, prompter_data, NULL, banner, 1, &prompt);
495     k5_set_prompt_types(context, NULL);
496     if (retval != 0)
497         return retval;
498 
499     return 0;
500 }
501 
502 /* Forces the user to choose a single tokeninfo via prompting. */
503 static krb5_error_code
prompt_for_tokeninfo(krb5_context context,krb5_prompter_fct prompter,void * prompter_data,krb5_otp_tokeninfo ** tis,krb5_otp_tokeninfo ** out_ti)504 prompt_for_tokeninfo(krb5_context context, krb5_prompter_fct prompter,
505                      void *prompter_data, krb5_otp_tokeninfo **tis,
506                      krb5_otp_tokeninfo **out_ti)
507 {
508     char response[1024], *prompt;
509     krb5_otp_tokeninfo *ti = NULL;
510     krb5_error_code retval = 0;
511     struct k5buf buf;
512     size_t i = 0, j = 0;
513 
514     k5_buf_init_dynamic(&buf);
515     k5_buf_add(&buf, _("Please choose from the following:\n"));
516     for (i = 0; tis[i] != NULL; i++) {
517         k5_buf_add_fmt(&buf, "\t%ld. %s ", (long)(i + 1), _("Vendor:"));
518         k5_buf_add_len(&buf, tis[i]->vendor.data, tis[i]->vendor.length);
519         k5_buf_add(&buf, "\n");
520     }
521     prompt = k5_buf_cstring(&buf);
522     if (prompt == NULL)
523         return ENOMEM;
524 
525     do {
526         retval = doprompt(context, prompter, prompter_data, prompt,
527                           _("Enter #"), response, sizeof(response));
528         if (retval != 0)
529             goto cleanup;
530 
531         errno = 0;
532         j = strtoul(response, NULL, 0);
533         if (errno != 0) {
534             retval = errno;
535             goto cleanup;
536         }
537         if (j < 1 || j > i)
538             continue;
539 
540         ti = tis[--j];
541     } while (ti == NULL);
542 
543     *out_ti = ti;
544 
545 cleanup:
546     k5_buf_free(&buf);
547     return retval;
548 }
549 
550 /* Builds a challenge string from the given tokeninfo. */
551 static krb5_error_code
make_challenge(const krb5_otp_tokeninfo * ti,char ** challenge)552 make_challenge(const krb5_otp_tokeninfo *ti, char **challenge)
553 {
554     if (challenge == NULL)
555         return EINVAL;
556 
557     *challenge = NULL;
558 
559     if (ti == NULL || ti->challenge.data == NULL)
560         return 0;
561 
562     if (asprintf(challenge, "%s %.*s\n",
563                  _("OTP Challenge:"),
564                  ti->challenge.length,
565                  ti->challenge.data) < 0)
566         return ENOMEM;
567 
568     return 0;
569 }
570 
571 /* Determines if a pin is required. If it is, it will be prompted for. */
572 static inline krb5_error_code
collect_pin(krb5_context context,krb5_prompter_fct prompter,void * prompter_data,const krb5_otp_tokeninfo * ti,krb5_data * out_pin)573 collect_pin(krb5_context context, krb5_prompter_fct prompter,
574             void *prompter_data, const krb5_otp_tokeninfo *ti,
575             krb5_data *out_pin)
576 {
577     krb5_error_code retval;
578     char otppin[1024];
579     krb5_flags collect;
580     krb5_data pin;
581 
582     /* If no PIN will be collected, don't prompt. */
583     collect = ti->flags & (KRB5_OTP_FLAG_COLLECT_PIN |
584                            KRB5_OTP_FLAG_SEPARATE_PIN);
585     if (collect == 0) {
586         *out_pin = empty_data();
587         return 0;
588     }
589 
590     /* Collect the PIN. */
591     retval = doprompt(context, prompter, prompter_data, NULL,
592                       _("OTP Token PIN"), otppin, sizeof(otppin));
593     if (retval != 0)
594         return retval;
595 
596     /* Set the PIN. */
597     pin = make_data(strdup(otppin), strlen(otppin));
598     if (pin.data == NULL)
599         return ENOMEM;
600 
601     *out_pin = pin;
602     return 0;
603 }
604 
605 /* Builds a request using the specified tokeninfo, value and pin. */
606 static krb5_error_code
make_request(krb5_context ctx,krb5_otp_tokeninfo * ti,const krb5_data * value,const krb5_data * pin,krb5_pa_otp_req ** out_req)607 make_request(krb5_context ctx, krb5_otp_tokeninfo *ti, const krb5_data *value,
608              const krb5_data *pin, krb5_pa_otp_req **out_req)
609 {
610     krb5_pa_otp_req *req = NULL;
611     krb5_error_code retval = 0;
612 
613     if (ti == NULL)
614         return 0;
615 
616     if (ti->format == KRB5_OTP_FORMAT_BASE64)
617         return ENOTSUP;
618 
619     req = calloc(1, sizeof(krb5_pa_otp_req));
620     if (req == NULL)
621         return ENOMEM;
622 
623     req->flags = ti->flags & KRB5_OTP_FLAG_NEXTOTP;
624 
625     retval = krb5int_copy_data_contents(ctx, &ti->vendor, &req->vendor);
626     if (retval != 0)
627         goto error;
628 
629     req->format = ti->format;
630 
631     retval = krb5int_copy_data_contents(ctx, &ti->token_id, &req->token_id);
632     if (retval != 0)
633         goto error;
634 
635     retval = krb5int_copy_data_contents(ctx, &ti->alg_id, &req->alg_id);
636     if (retval != 0)
637         goto error;
638 
639     retval = krb5int_copy_data_contents(ctx, value, &req->otp_value);
640     if (retval != 0)
641         goto error;
642 
643     if (ti->flags & KRB5_OTP_FLAG_COLLECT_PIN) {
644         if (ti->flags & KRB5_OTP_FLAG_SEPARATE_PIN) {
645             if (pin == NULL || pin->data == NULL) {
646                 retval = EINVAL; /* No pin found! */
647                 goto error;
648             }
649 
650             retval = krb5int_copy_data_contents(ctx, pin, &req->pin);
651             if (retval != 0)
652                 goto error;
653         } else if (pin != NULL && pin->data != NULL) {
654             krb5_free_data_contents(ctx, &req->otp_value);
655             retval = asprintf(&req->otp_value.data, "%.*s%.*s",
656                               pin->length, pin->data,
657                               value->length, value->data);
658             if (retval < 0) {
659                 retval = ENOMEM;
660                 req->otp_value = empty_data();
661                 goto error;
662             }
663             req->otp_value.length = req->pin.length + req->otp_value.length;
664         } /* Otherwise, the responder has already combined them. */
665     }
666 
667     *out_req = req;
668     return 0;
669 
670 error:
671     k5_free_pa_otp_req(ctx, req);
672     return retval;
673 }
674 
675 /*
676  * Filters a set of tokeninfos given an otp value.  If the set is reduced to
677  * a single tokeninfo, it will be set in out_ti.  Otherwise, a new shallow copy
678  * will be allocated in out_filtered.
679  */
680 static inline krb5_error_code
filter_tokeninfos(krb5_context context,const char * otpvalue,krb5_otp_tokeninfo ** tis,krb5_otp_tokeninfo *** out_filtered,krb5_otp_tokeninfo ** out_ti)681 filter_tokeninfos(krb5_context context, const char *otpvalue,
682                   krb5_otp_tokeninfo **tis,
683                   krb5_otp_tokeninfo ***out_filtered,
684                   krb5_otp_tokeninfo **out_ti)
685 {
686     krb5_otp_tokeninfo **filtered;
687     size_t i = 0, j = 0;
688 
689     while (tis[i] != NULL)
690         i++;
691 
692     filtered = calloc(i + 1, sizeof(const krb5_otp_tokeninfo *));
693     if (filtered == NULL)
694         return ENOMEM;
695 
696     /* Make a list of tokeninfos that match the value. */
697     for (i = 0, j = 0; tis[i] != NULL; i++) {
698         if (otpvalue_matches_tokeninfo(otpvalue, tis[i]))
699             filtered[j++] = tis[i];
700     }
701 
702     /* It is an error if we have no matching tokeninfos. */
703     if (filtered[0] == NULL) {
704         free(filtered);
705         k5_setmsg(context, KRB5_PREAUTH_FAILED,
706                   _("OTP value doesn't match any token formats"));
707         return KRB5_PREAUTH_FAILED; /* We have no supported tokeninfos. */
708     }
709 
710     /* Otherwise, if we have just one tokeninfo, choose it. */
711     if (filtered[1] == NULL) {
712         *out_ti = filtered[0];
713         *out_filtered = NULL;
714         free(filtered);
715         return 0;
716     }
717 
718     /* Otherwise, we'll return the remaining list. */
719     *out_ti = NULL;
720     *out_filtered = filtered;
721     return 0;
722 }
723 
724 /* Outputs the selected tokeninfo and possibly a value and pin.
725  * Prompting may occur. */
726 static krb5_error_code
prompt_for_token(krb5_context context,krb5_prompter_fct prompter,void * prompter_data,krb5_otp_tokeninfo ** tis,krb5_otp_tokeninfo ** out_ti,krb5_data * out_value,krb5_data * out_pin)727 prompt_for_token(krb5_context context, krb5_prompter_fct prompter,
728                  void *prompter_data, krb5_otp_tokeninfo **tis,
729                  krb5_otp_tokeninfo **out_ti, krb5_data *out_value,
730                  krb5_data *out_pin)
731 {
732     krb5_otp_tokeninfo **filtered = NULL;
733     krb5_otp_tokeninfo *ti = NULL;
734     krb5_error_code retval;
735     size_t i, challengers = 0;
736     char *challenge = NULL;
737     char otpvalue[1024];
738     krb5_data value, pin;
739 
740     memset(otpvalue, 0, sizeof(otpvalue));
741 
742     if (tis == NULL || tis[0] == NULL || out_ti == NULL)
743         return EINVAL;
744 
745     /* Count how many challenges we have. */
746     for (i = 0; tis[i] != NULL; i++) {
747         if (tis[i]->challenge.data != NULL)
748             challengers++;
749     }
750 
751     /* If we have only one tokeninfo as input, choose it. */
752     if (i == 1)
753         ti = tis[0];
754 
755     /* Setup our challenge, if present. */
756     if (challengers > 0) {
757         /* If we have multiple tokeninfos still, choose now. */
758         if (ti == NULL) {
759             retval = prompt_for_tokeninfo(context, prompter, prompter_data,
760                                           tis, &ti);
761             if (retval != 0)
762                 return retval;
763         }
764 
765         /* Create the challenge prompt. */
766         retval = make_challenge(ti, &challenge);
767         if (retval != 0)
768             return retval;
769     }
770 
771     /* Prompt for token value. */
772     retval = doprompt(context, prompter, prompter_data, challenge,
773                       _("Enter OTP Token Value"), otpvalue, sizeof(otpvalue));
774     free(challenge);
775     if (retval != 0)
776         return retval;
777 
778     if (ti == NULL) {
779         /* Filter out tokeninfos that don't match our token value. */
780         retval = filter_tokeninfos(context, otpvalue, tis, &filtered, &ti);
781         if (retval != 0)
782             return retval;
783 
784         /* If we still don't have a single tokeninfo, choose now. */
785         if (filtered != NULL) {
786             retval = prompt_for_tokeninfo(context, prompter, prompter_data,
787                                           filtered, &ti);
788             free(filtered);
789             if (retval != 0)
790                 return retval;
791         }
792     }
793 
794     assert(ti != NULL);
795 
796     /* Set the value. */
797     value = make_data(strdup(otpvalue), strlen(otpvalue));
798     if (value.data == NULL)
799         return ENOMEM;
800 
801     /* Collect the PIN, if necessary. */
802     retval = collect_pin(context, prompter, prompter_data, ti, &pin);
803     if (retval != 0) {
804         krb5_free_data_contents(context, &value);
805         return retval;
806     }
807 
808     *out_value = value;
809     *out_pin = pin;
810     *out_ti = ti;
811     return 0;
812 }
813 
814 /* Encode the OTP request into a krb5_pa_data buffer. */
815 static krb5_error_code
set_pa_data(const krb5_pa_otp_req * req,krb5_pa_data *** pa_data_out)816 set_pa_data(const krb5_pa_otp_req *req, krb5_pa_data ***pa_data_out)
817 {
818     krb5_pa_data **out = NULL;
819     krb5_data *tmp;
820 
821     /* Allocate the preauth data array and one item. */
822     out = calloc(2, sizeof(krb5_pa_data *));
823     if (out == NULL)
824         goto error;
825     out[0] = calloc(1, sizeof(krb5_pa_data));
826     out[1] = NULL;
827     if (out[0] == NULL)
828         goto error;
829 
830     /* Encode our request into the preauth data item. */
831     memset(out[0], 0, sizeof(krb5_pa_data));
832     out[0]->pa_type = KRB5_PADATA_OTP_REQUEST;
833     if (encode_krb5_pa_otp_req(req, &tmp) != 0)
834         goto error;
835     out[0]->contents = (krb5_octet *)tmp->data;
836     out[0]->length = tmp->length;
837     free(tmp);
838 
839     *pa_data_out = out;
840     return 0;
841 
842 error:
843     if (out != NULL) {
844         free(out[0]);
845         free(out);
846     }
847     return ENOMEM;
848 }
849 
850 /* Tests krb5_data to see if it is printable. */
851 static krb5_boolean
is_printable_string(const krb5_data * data)852 is_printable_string(const krb5_data *data)
853 {
854     unsigned int i;
855 
856     if (data == NULL)
857         return FALSE;
858 
859     for (i = 0; i < data->length; i++) {
860         if (!isprint((unsigned char)data->data[i]))
861             return FALSE;
862     }
863 
864     return TRUE;
865 }
866 
867 /* Returns TRUE when the given tokeninfo contains the subset of features we
868  * support. */
869 static krb5_boolean
is_tokeninfo_supported(krb5_otp_tokeninfo * ti)870 is_tokeninfo_supported(krb5_otp_tokeninfo *ti)
871 {
872     krb5_flags supported_flags = KRB5_OTP_FLAG_COLLECT_PIN |
873                                  KRB5_OTP_FLAG_NO_COLLECT_PIN |
874                                  KRB5_OTP_FLAG_SEPARATE_PIN;
875 
876     /* Flags we don't support... */
877     if (ti->flags & ~supported_flags)
878         return FALSE;
879 
880     /* We don't currently support hashing. */
881     if (ti->supported_hash_alg != NULL || ti->iteration_count >= 0)
882         return FALSE;
883 
884     /* Remove tokeninfos with invalid vendor strings. */
885     if (!is_printable_string(&ti->vendor))
886         return FALSE;
887 
888     /* Remove tokeninfos with non-printable challenges. */
889     if (!is_printable_string(&ti->challenge))
890         return FALSE;
891 
892     /* We don't currently support base64. */
893     if (ti->format == KRB5_OTP_FORMAT_BASE64)
894         return FALSE;
895 
896     return TRUE;
897 }
898 
899 /* Removes unsupported tokeninfos. Returns an error if no tokeninfos remain. */
900 static krb5_error_code
filter_supported_tokeninfos(krb5_context context,krb5_otp_tokeninfo ** tis)901 filter_supported_tokeninfos(krb5_context context, krb5_otp_tokeninfo **tis)
902 {
903     size_t i, j;
904 
905     /* Filter out any tokeninfos we don't support. */
906     for (i = 0, j = 0; tis[i] != NULL; i++) {
907         if (!is_tokeninfo_supported(tis[i]))
908             k5_free_otp_tokeninfo(context, tis[i]);
909         else
910             tis[j++] = tis[i];
911     }
912 
913     /* Terminate the array. */
914     tis[j] = NULL;
915 
916     if (tis[0] != NULL)
917         return 0;
918 
919     k5_setmsg(context, KRB5_PREAUTH_FAILED, _("No supported tokens"));
920     return KRB5_PREAUTH_FAILED; /* We have no supported tokeninfos. */
921 }
922 
923 /*
924  * Try to find tokeninfos which match configuration data recorded in the input
925  * ccache, and if exactly one is found, drop the rest.
926  */
927 static krb5_error_code
filter_config_tokeninfos(krb5_context context,krb5_clpreauth_callbacks cb,krb5_clpreauth_rock rock,krb5_otp_tokeninfo ** tis)928 filter_config_tokeninfos(krb5_context context,
929                          krb5_clpreauth_callbacks cb,
930                          krb5_clpreauth_rock rock,
931                          krb5_otp_tokeninfo **tis)
932 {
933     krb5_otp_tokeninfo *match = NULL;
934     size_t i, j;
935     const char *vendor, *alg_id, *token_id;
936 
937     /* Pull up what we know about the token we want to use. */
938     vendor = cb->get_cc_config(context, rock, "vendor");
939     alg_id = cb->get_cc_config(context, rock, "algID");
940     token_id = cb->get_cc_config(context, rock, "tokenID");
941 
942     /* Look for a single matching entry. */
943     for (i = 0; tis[i] != NULL; i++) {
944         if (vendor != NULL && tis[i]->vendor.length > 0 &&
945             !data_eq_string(tis[i]->vendor, vendor))
946             continue;
947         if (alg_id != NULL && tis[i]->alg_id.length > 0 &&
948             !data_eq_string(tis[i]->alg_id, alg_id))
949             continue;
950         if (token_id != NULL && tis[i]->token_id.length > 0 &&
951             !data_eq_string(tis[i]->token_id, token_id))
952             continue;
953         /* Oh, we already had a matching entry. More than one -> no change. */
954         if (match != NULL)
955             return 0;
956         match = tis[i];
957     }
958 
959     /* No matching entry -> no change. */
960     if (match == NULL)
961         return 0;
962 
963     /* Prune out everything except the best match. */
964     for (i = 0, j = 0; tis[i] != NULL; i++) {
965         if (tis[i] != match)
966             k5_free_otp_tokeninfo(context, tis[i]);
967         else
968             tis[j++] = tis[i];
969     }
970     tis[j] = NULL;
971 
972     return 0;
973 }
974 
975 static void
otp_client_request_init(krb5_context context,krb5_clpreauth_moddata moddata,krb5_clpreauth_modreq * modreq_out)976 otp_client_request_init(krb5_context context, krb5_clpreauth_moddata moddata,
977                         krb5_clpreauth_modreq *modreq_out)
978 {
979     *modreq_out = calloc(1, sizeof(krb5_pa_otp_challenge *));
980 }
981 
982 static krb5_error_code
otp_client_prep_questions(krb5_context context,krb5_clpreauth_moddata moddata,krb5_clpreauth_modreq modreq,krb5_get_init_creds_opt * opt,krb5_clpreauth_callbacks cb,krb5_clpreauth_rock rock,krb5_kdc_req * request,krb5_data * encoded_request_body,krb5_data * encoded_previous_request,krb5_pa_data * pa_data)983 otp_client_prep_questions(krb5_context context, krb5_clpreauth_moddata moddata,
984                           krb5_clpreauth_modreq modreq,
985                           krb5_get_init_creds_opt *opt,
986                           krb5_clpreauth_callbacks cb,
987                           krb5_clpreauth_rock rock, krb5_kdc_req *request,
988                           krb5_data *encoded_request_body,
989                           krb5_data *encoded_previous_request,
990                           krb5_pa_data *pa_data)
991 {
992     krb5_pa_otp_challenge *chl;
993     krb5_error_code retval;
994     krb5_data tmp;
995     char *json;
996 
997     if (modreq == NULL)
998         return ENOMEM;
999 
1000     /* Decode the challenge. */
1001     tmp = make_data(pa_data->contents, pa_data->length);
1002     retval = decode_krb5_pa_otp_challenge(&tmp,
1003                                           (krb5_pa_otp_challenge **)modreq);
1004     if (retval != 0)
1005         return retval;
1006     chl = *(krb5_pa_otp_challenge **)modreq;
1007 
1008     /* Remove unsupported tokeninfos. */
1009     retval = filter_supported_tokeninfos(context, chl->tokeninfo);
1010     if (retval != 0)
1011         return retval;
1012 
1013     /* Remove tokeninfos that don't match the recorded description, if that
1014      * results in there being only one that does. */
1015     retval = filter_config_tokeninfos(context, cb, rock, chl->tokeninfo);
1016     if (retval != 0)
1017         return retval;
1018 
1019     /* Make the JSON representation. */
1020     retval = codec_encode_challenge(context, chl, &json);
1021     if (retval != 0)
1022         return retval;
1023 
1024     /* Ask the question. */
1025     retval = cb->ask_responder_question(context, rock,
1026                                         KRB5_RESPONDER_QUESTION_OTP,
1027                                         json);
1028     free(json);
1029     return retval;
1030 }
1031 
1032 /*
1033  * Save the vendor, algID, and tokenID values for the selected token to the
1034  * out_ccache, so that later we can try to use them to select the right one
1035  * without having ot ask the user.
1036  */
1037 static void
save_config_tokeninfo(krb5_context context,krb5_clpreauth_callbacks cb,krb5_clpreauth_rock rock,krb5_otp_tokeninfo * ti)1038 save_config_tokeninfo(krb5_context context,
1039                       krb5_clpreauth_callbacks cb,
1040                       krb5_clpreauth_rock rock,
1041                       krb5_otp_tokeninfo *ti)
1042 {
1043     char *tmp;
1044     if (ti->vendor.length > 0 &&
1045         asprintf(&tmp, "%.*s", ti->vendor.length, ti->vendor.data) >= 0) {
1046         cb->set_cc_config(context, rock, "vendor", tmp);
1047         free(tmp);
1048     }
1049     if (ti->alg_id.length > 0 &&
1050         asprintf(&tmp, "%.*s", ti->alg_id.length, ti->alg_id.data) >= 0) {
1051         cb->set_cc_config(context, rock, "algID", tmp);
1052         free(tmp);
1053     }
1054     if (ti->token_id.length > 0 &&
1055         asprintf(&tmp, "%.*s", ti->token_id.length, ti->token_id.data) >= 0) {
1056         cb->set_cc_config(context, rock, "tokenID", tmp);
1057         free(tmp);
1058     }
1059 }
1060 
1061 static krb5_error_code
otp_client_process(krb5_context context,krb5_clpreauth_moddata moddata,krb5_clpreauth_modreq modreq,krb5_get_init_creds_opt * opt,krb5_clpreauth_callbacks cb,krb5_clpreauth_rock rock,krb5_kdc_req * request,krb5_data * encoded_request_body,krb5_data * encoded_previous_request,krb5_pa_data * pa_data,krb5_prompter_fct prompter,void * prompter_data,krb5_pa_data *** pa_data_out)1062 otp_client_process(krb5_context context, krb5_clpreauth_moddata moddata,
1063                    krb5_clpreauth_modreq modreq, krb5_get_init_creds_opt *opt,
1064                    krb5_clpreauth_callbacks cb, krb5_clpreauth_rock rock,
1065                    krb5_kdc_req *request, krb5_data *encoded_request_body,
1066                    krb5_data *encoded_previous_request, krb5_pa_data *pa_data,
1067                    krb5_prompter_fct prompter, void *prompter_data,
1068                    krb5_pa_data ***pa_data_out)
1069 {
1070     krb5_pa_otp_challenge *chl = NULL;
1071     krb5_otp_tokeninfo *ti = NULL;
1072     krb5_keyblock *as_key = NULL;
1073     krb5_pa_otp_req *req = NULL;
1074     krb5_error_code retval = 0;
1075     krb5_data value, pin;
1076     const char *answer;
1077 
1078     if (modreq == NULL)
1079         return ENOMEM;
1080     chl = *(krb5_pa_otp_challenge **)modreq;
1081 
1082     *pa_data_out = NULL;
1083 
1084     /* Get FAST armor key. */
1085     as_key = cb->fast_armor(context, rock);
1086     if (as_key == NULL)
1087         return ENOENT;
1088 
1089     /* Attempt to get token selection from the responder. */
1090     pin = empty_data();
1091     value = empty_data();
1092     answer = cb->get_responder_answer(context, rock,
1093                                       KRB5_RESPONDER_QUESTION_OTP);
1094     retval = codec_decode_answer(context, answer, chl->tokeninfo, &ti, &value,
1095                                  &pin);
1096     if (retval != 0) {
1097         /* If the responder doesn't have a token selection,
1098          * we need to select the token via prompting. */
1099         retval = prompt_for_token(context, prompter, prompter_data,
1100                                   chl->tokeninfo, &ti, &value, &pin);
1101         if (retval != 0)
1102             goto error;
1103     }
1104 
1105     /* Make the request. */
1106     retval = make_request(context, ti, &value, &pin, &req);
1107     if (retval != 0)
1108         goto error;
1109 
1110     /* Save information about the token which was used. */
1111     save_config_tokeninfo(context, cb, rock, ti);
1112 
1113     /* Encrypt the challenge's nonce and set it in the request. */
1114     retval = encrypt_nonce(context, as_key, chl, req);
1115     if (retval != 0)
1116         goto error;
1117 
1118     /* Use FAST armor key as response key. */
1119     retval = cb->set_as_key(context, rock, as_key);
1120     if (retval != 0)
1121         goto error;
1122 
1123     /* Encode the request into the pa_data output. */
1124     retval = set_pa_data(req, pa_data_out);
1125     if (retval != 0)
1126         goto error;
1127     cb->disable_fallback(context, rock);
1128 
1129 error:
1130     krb5_free_data_contents(context, &value);
1131     krb5_free_data_contents(context, &pin);
1132     k5_free_pa_otp_req(context, req);
1133     return retval;
1134 }
1135 
1136 static void
otp_client_request_fini(krb5_context context,krb5_clpreauth_moddata moddata,krb5_clpreauth_modreq modreq)1137 otp_client_request_fini(krb5_context context, krb5_clpreauth_moddata moddata,
1138                         krb5_clpreauth_modreq modreq)
1139 {
1140     if (modreq == NULL)
1141         return;
1142 
1143     k5_free_pa_otp_challenge(context, *(krb5_pa_otp_challenge **)modreq);
1144     free(modreq);
1145 }
1146 
1147 krb5_error_code
clpreauth_otp_initvt(krb5_context context,int maj_ver,int min_ver,krb5_plugin_vtable vtable)1148 clpreauth_otp_initvt(krb5_context context, int maj_ver, int min_ver,
1149                      krb5_plugin_vtable vtable)
1150 {
1151     krb5_clpreauth_vtable vt;
1152 
1153     if (maj_ver != 1)
1154         return KRB5_PLUGIN_VER_NOTSUPP;
1155 
1156     vt = (krb5_clpreauth_vtable)vtable;
1157     vt->name = "otp";
1158     vt->pa_type_list = otp_client_supported_pa_types;
1159     vt->request_init = otp_client_request_init;
1160     vt->prep_questions = otp_client_prep_questions;
1161     vt->process = otp_client_process;
1162     vt->request_fini = otp_client_request_fini;
1163     vt->gic_opts = NULL;
1164 
1165     return 0;
1166 }
1167 
1168 krb5_error_code KRB5_CALLCONV
krb5_responder_otp_get_challenge(krb5_context ctx,krb5_responder_context rctx,krb5_responder_otp_challenge ** chl)1169 krb5_responder_otp_get_challenge(krb5_context ctx,
1170                                  krb5_responder_context rctx,
1171                                  krb5_responder_otp_challenge **chl)
1172 {
1173     const char *answer;
1174     krb5_responder_otp_challenge *challenge;
1175 
1176     answer = krb5_responder_get_challenge(ctx, rctx,
1177                                           KRB5_RESPONDER_QUESTION_OTP);
1178     if (answer == NULL) {
1179         *chl = NULL;
1180         return 0;
1181     }
1182 
1183     challenge = codec_decode_challenge(ctx, answer);
1184     if (challenge == NULL)
1185         return ENOMEM;
1186 
1187     *chl = challenge;
1188     return 0;
1189 }
1190 
1191 krb5_error_code KRB5_CALLCONV
krb5_responder_otp_set_answer(krb5_context ctx,krb5_responder_context rctx,size_t ti,const char * value,const char * pin)1192 krb5_responder_otp_set_answer(krb5_context ctx, krb5_responder_context rctx,
1193                               size_t ti, const char *value, const char *pin)
1194 {
1195     krb5_error_code retval;
1196     k5_json_object obj = NULL;
1197     k5_json_number num;
1198     k5_json_string str;
1199     char *tmp;
1200 
1201     retval = k5_json_object_create(&obj);
1202     if (retval != 0)
1203         goto error;
1204 
1205     retval = k5_json_number_create(ti, &num);
1206     if (retval != 0)
1207         goto error;
1208 
1209     retval = k5_json_object_set(obj, "tokeninfo", num);
1210     k5_json_release(num);
1211     if (retval != 0)
1212         goto error;
1213 
1214     if (value != NULL) {
1215         retval = k5_json_string_create(value, &str);
1216         if (retval != 0)
1217             goto error;
1218 
1219         retval = k5_json_object_set(obj, "value", str);
1220         k5_json_release(str);
1221         if (retval != 0)
1222             goto error;
1223     }
1224 
1225     if (pin != NULL) {
1226         retval = k5_json_string_create(pin, &str);
1227         if (retval != 0)
1228             goto error;
1229 
1230         retval = k5_json_object_set(obj, "pin", str);
1231         k5_json_release(str);
1232         if (retval != 0)
1233             goto error;
1234     }
1235 
1236     retval = k5_json_encode(obj, &tmp);
1237     if (retval != 0)
1238         goto error;
1239     k5_json_release(obj);
1240 
1241     retval = krb5_responder_set_answer(ctx, rctx, KRB5_RESPONDER_QUESTION_OTP,
1242                                        tmp);
1243     free(tmp);
1244     return retval;
1245 
1246 error:
1247     k5_json_release(obj);
1248     return retval;
1249 }
1250 
1251 void KRB5_CALLCONV
krb5_responder_otp_challenge_free(krb5_context ctx,krb5_responder_context rctx,krb5_responder_otp_challenge * chl)1252 krb5_responder_otp_challenge_free(krb5_context ctx,
1253                                   krb5_responder_context rctx,
1254                                   krb5_responder_otp_challenge *chl)
1255 {
1256     size_t i;
1257 
1258     if (chl == NULL)
1259         return;
1260 
1261     for (i = 0; chl->tokeninfo[i]; i++)
1262         free_tokeninfo(chl->tokeninfo[i]);
1263     free(chl->service);
1264     free(chl->tokeninfo);
1265     free(chl);
1266 }
1267