48fcbb5d53daa166c695f80b4399ee0b9616fc75
[platform/upstream/krb5.git] / src / lib / krb5 / krb / preauth_otp.c
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
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
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
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
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
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
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
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
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     int 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 *
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 *
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
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, i;
382     krb5_data tmp;
383
384     if (answer == NULL)
385         return EBADMSG;
386
387     retval = k5_json_decode(answer, &val);
388     if (retval != 0)
389         goto cleanup;
390
391     if (k5_json_get_tid(val) != K5_JSON_TID_OBJECT)
392         goto cleanup;
393
394     retval = codec_value_to_int32(val, "tokeninfo", &indx);
395     if (retval != 0)
396         goto cleanup;
397
398     for (i = 0; tis[i] != NULL; i++) {
399         if (i == indx) {
400             retval = codec_value_to_data(val, "value", &tmp);
401             if (retval != 0 && retval != ENOENT)
402                 goto cleanup;
403
404             retval = codec_value_to_data(val, "pin", pin);
405             if (retval != 0 && retval != ENOENT) {
406                 krb5_free_data_contents(context, &tmp);
407                 goto cleanup;
408             }
409
410             *value = tmp;
411             *ti = tis[i];
412             retval = 0;
413             goto cleanup;
414         }
415     }
416     retval = EINVAL;
417
418 cleanup:
419     k5_json_release(val);
420     return retval;
421 }
422
423 /* Takes the nonce from the challenge and encrypts it into the request. */
424 static krb5_error_code
425 encrypt_nonce(krb5_context ctx, krb5_keyblock *key,
426               const krb5_pa_otp_challenge *chl, krb5_pa_otp_req *req)
427 {
428     krb5_error_code retval;
429     krb5_enc_data encdata;
430     krb5_data *er;
431
432     /* Encode the nonce. */
433     retval = encode_krb5_pa_otp_enc_req(&chl->nonce, &er);
434     if (retval != 0)
435         return retval;
436
437     /* Do the encryption. */
438     retval = krb5_encrypt_helper(ctx, key, KRB5_KEYUSAGE_PA_OTP_REQUEST,
439                                  er, &encdata);
440     krb5_free_data(ctx, er);
441     if (retval != 0)
442         return retval;
443
444     req->enc_data = encdata;
445     return 0;
446 }
447
448 /* Checks to see if the user-supplied otp value matches the length and format
449  * of the supplied tokeninfo. */
450 static int
451 otpvalue_matches_tokeninfo(const char *otpvalue, krb5_otp_tokeninfo *ti)
452 {
453     int (*table[])(int c) = { isdigit, isxdigit, isalnum };
454
455     if (otpvalue == NULL || ti == NULL)
456         return 0;
457
458     if (ti->length >= 0 && strlen(otpvalue) != (size_t)ti->length)
459         return 0;
460
461     if (ti->format >= 0 && ti->format < 3) {
462         while (*otpvalue) {
463             if (!(*table[ti->format])((unsigned char)*otpvalue++))
464                 return 0;
465         }
466     }
467
468     return 1;
469 }
470
471 /* Performs a prompt and saves the response in the out parameter. */
472 static krb5_error_code
473 doprompt(krb5_context context, krb5_prompter_fct prompter, void *prompter_data,
474          const char *banner, const char *prompttxt, char *out, size_t len)
475 {
476     krb5_prompt prompt;
477     krb5_data prompt_reply;
478     krb5_error_code retval;
479     krb5_prompt_type prompt_type = KRB5_PROMPT_TYPE_PREAUTH;
480
481     if (prompttxt == NULL || out == NULL)
482         return EINVAL;
483
484     memset(out, 0, len);
485
486     prompt_reply = make_data(out, len);
487     prompt.reply = &prompt_reply;
488     prompt.prompt = (char *)prompttxt;
489     prompt.hidden = 1;
490
491     /* PROMPTER_INVOCATION */
492     k5_set_prompt_types(context, &prompt_type);
493     retval = (*prompter)(context, prompter_data, NULL, banner, 1, &prompt);
494     k5_set_prompt_types(context, NULL);
495     if (retval != 0)
496         return retval;
497
498     return 0;
499 }
500
501 /* Forces the user to choose a single tokeninfo via prompting. */
502 static krb5_error_code
503 prompt_for_tokeninfo(krb5_context context, krb5_prompter_fct prompter,
504                      void *prompter_data, krb5_otp_tokeninfo **tis,
505                      krb5_otp_tokeninfo **out_ti)
506 {
507     char *banner = NULL, *tmp, response[1024];
508     krb5_otp_tokeninfo *ti = NULL;
509     krb5_error_code retval = 0;
510     int i = 0, j = 0;
511
512     for (i = 0; tis[i] != NULL; i++) {
513         if (asprintf(&tmp, "%s\t%d. %s %.*s\n",
514                      banner ? banner :
515                          _("Please choose from the following:\n"),
516                      i + 1, _("Vendor:"), tis[i]->vendor.length,
517                      tis[i]->vendor.data) < 0) {
518             free(banner);
519             return ENOMEM;
520         }
521
522         free(banner);
523         banner = tmp;
524     }
525
526     do {
527         retval = doprompt(context, prompter, prompter_data,
528                           banner, _("Enter #"), response, sizeof(response));
529         if (retval != 0) {
530             free(banner);
531             return retval;
532         }
533
534         errno = 0;
535         j = strtol(response, NULL, 0);
536         if (errno != 0) {
537             free(banner);
538             return errno;
539         }
540         if (j < 1 || j > i)
541             continue;
542
543         ti = tis[--j];
544     } while (ti == NULL);
545
546     free(banner);
547     *out_ti = ti;
548     return 0;
549 }
550
551 /* Builds a challenge string from the given tokeninfo. */
552 static krb5_error_code
553 make_challenge(const krb5_otp_tokeninfo *ti, char **challenge)
554 {
555     if (challenge == NULL)
556         return EINVAL;
557
558     *challenge = NULL;
559
560     if (ti == NULL || ti->challenge.data == NULL)
561         return 0;
562
563     if (asprintf(challenge, "%s %.*s\n",
564                  _("OTP Challenge:"),
565                  ti->challenge.length,
566                  ti->challenge.data) < 0)
567         return ENOMEM;
568
569     return 0;
570 }
571
572 /* Determines if a pin is required. If it is, it will be prompted for. */
573 static inline krb5_error_code
574 collect_pin(krb5_context context, krb5_prompter_fct prompter,
575             void *prompter_data, const krb5_otp_tokeninfo *ti,
576             krb5_data *out_pin)
577 {
578     krb5_error_code retval;
579     char otppin[1024];
580     krb5_flags collect;
581     krb5_data pin;
582
583     /* If no PIN will be collected, don't prompt. */
584     collect = ti->flags & (KRB5_OTP_FLAG_COLLECT_PIN |
585                            KRB5_OTP_FLAG_SEPARATE_PIN);
586     if (collect == 0) {
587         *out_pin = empty_data();
588         return 0;
589     }
590
591     /* Collect the PIN. */
592     retval = doprompt(context, prompter, prompter_data, NULL,
593                       _("OTP Token PIN"), otppin, sizeof(otppin));
594     if (retval != 0)
595         return retval;
596
597     /* Set the PIN. */
598     pin = make_data(strdup(otppin), strlen(otppin));
599     if (pin.data == NULL)
600         return ENOMEM;
601
602     *out_pin = pin;
603     return 0;
604 }
605
606 /* Builds a request using the specified tokeninfo, value and pin. */
607 static krb5_error_code
608 make_request(krb5_context ctx, krb5_otp_tokeninfo *ti, const krb5_data *value,
609              const krb5_data *pin, krb5_pa_otp_req **out_req)
610 {
611     krb5_pa_otp_req *req = NULL;
612     krb5_error_code retval = 0;
613
614     if (ti == NULL)
615         return 0;
616
617     if (ti->format == KRB5_OTP_FORMAT_BASE64)
618         return ENOTSUP;
619
620     req = calloc(1, sizeof(krb5_pa_otp_req));
621     if (req == NULL)
622         return ENOMEM;
623
624     req->flags = ti->flags & KRB5_OTP_FLAG_NEXTOTP;
625
626     retval = krb5int_copy_data_contents(ctx, &ti->vendor, &req->vendor);
627     if (retval != 0)
628         goto error;
629
630     req->format = ti->format;
631
632     retval = krb5int_copy_data_contents(ctx, &ti->token_id, &req->token_id);
633     if (retval != 0)
634         goto error;
635
636     retval = krb5int_copy_data_contents(ctx, &ti->alg_id, &req->alg_id);
637     if (retval != 0)
638         goto error;
639
640     retval = krb5int_copy_data_contents(ctx, value, &req->otp_value);
641     if (retval != 0)
642         goto error;
643
644     if (ti->flags & KRB5_OTP_FLAG_COLLECT_PIN) {
645         if (ti->flags & KRB5_OTP_FLAG_SEPARATE_PIN) {
646             if (pin == NULL || pin->data == NULL) {
647                 retval = EINVAL; /* No pin found! */
648                 goto error;
649             }
650
651             retval = krb5int_copy_data_contents(ctx, pin, &req->pin);
652             if (retval != 0)
653                 goto error;
654         } else if (pin != NULL && pin->data != NULL) {
655             krb5_free_data_contents(ctx, &req->otp_value);
656             retval = asprintf(&req->otp_value.data, "%.*s%.*s",
657                               pin->length, pin->data,
658                               value->length, value->data);
659             if (retval < 0) {
660                 retval = ENOMEM;
661                 req->otp_value = empty_data();
662                 goto error;
663             }
664             req->otp_value.length = req->pin.length + req->otp_value.length;
665         } /* Otherwise, the responder has already combined them. */
666     }
667
668     *out_req = req;
669     return 0;
670
671 error:
672     k5_free_pa_otp_req(ctx, req);
673     return retval;
674 }
675
676 /*
677  * Filters a set of tokeninfos given an otp value.  If the set is reduced to
678  * a single tokeninfo, it will be set in out_ti.  Otherwise, a new shallow copy
679  * will be allocated in out_filtered.
680  */
681 static inline krb5_error_code
682 filter_tokeninfos(krb5_context context, const char *otpvalue,
683                   krb5_otp_tokeninfo **tis,
684                   krb5_otp_tokeninfo ***out_filtered,
685                   krb5_otp_tokeninfo **out_ti)
686 {
687     krb5_otp_tokeninfo **filtered;
688     size_t i = 0, j = 0;
689
690     while (tis[i] != NULL)
691         i++;
692
693     filtered = calloc(i + 1, sizeof(const krb5_otp_tokeninfo *));
694     if (filtered == NULL)
695         return ENOMEM;
696
697     /* Make a list of tokeninfos that match the value. */
698     for (i = 0, j = 0; tis[i] != NULL; i++) {
699         if (otpvalue_matches_tokeninfo(otpvalue, tis[i]))
700             filtered[j++] = tis[i];
701     }
702
703     /* It is an error if we have no matching tokeninfos. */
704     if (filtered[0] == NULL) {
705         free(filtered);
706         k5_setmsg(context, KRB5_PREAUTH_FAILED,
707                   _("OTP value doesn't match any token formats"));
708         return KRB5_PREAUTH_FAILED; /* We have no supported tokeninfos. */
709     }
710
711     /* Otherwise, if we have just one tokeninfo, choose it. */
712     if (filtered[1] == NULL) {
713         *out_ti = filtered[0];
714         *out_filtered = NULL;
715         free(filtered);
716         return 0;
717     }
718
719     /* Otherwise, we'll return the remaining list. */
720     *out_ti = NULL;
721     *out_filtered = filtered;
722     return 0;
723 }
724
725 /* Outputs the selected tokeninfo and possibly a value and pin.
726  * Prompting may occur. */
727 static krb5_error_code
728 prompt_for_token(krb5_context context, krb5_prompter_fct prompter,
729                  void *prompter_data, krb5_otp_tokeninfo **tis,
730                  krb5_otp_tokeninfo **out_ti, krb5_data *out_value,
731                  krb5_data *out_pin)
732 {
733     krb5_otp_tokeninfo **filtered = NULL;
734     krb5_otp_tokeninfo *ti = NULL;
735     krb5_error_code retval;
736     int i, challengers = 0;
737     char *challenge = NULL;
738     char otpvalue[1024];
739     krb5_data value, pin;
740
741     memset(otpvalue, 0, sizeof(otpvalue));
742
743     if (tis == NULL || tis[0] == NULL || out_ti == NULL)
744         return EINVAL;
745
746     /* Count how many challenges we have. */
747     for (i = 0; tis[i] != NULL; i++) {
748         if (tis[i]->challenge.data != NULL)
749             challengers++;
750     }
751
752     /* If we have only one tokeninfo as input, choose it. */
753     if (i == 1)
754         ti = tis[0];
755
756     /* Setup our challenge, if present. */
757     if (challengers > 0) {
758         /* If we have multiple tokeninfos still, choose now. */
759         if (ti == NULL) {
760             retval = prompt_for_tokeninfo(context, prompter, prompter_data,
761                                           tis, &ti);
762             if (retval != 0)
763                 return retval;
764         }
765
766         /* Create the challenge prompt. */
767         retval = make_challenge(ti, &challenge);
768         if (retval != 0)
769             return retval;
770     }
771
772     /* Prompt for token value. */
773     retval = doprompt(context, prompter, prompter_data, challenge,
774                       _("Enter OTP Token Value"), otpvalue, sizeof(otpvalue));
775     free(challenge);
776     if (retval != 0)
777         return retval;
778
779     if (ti == NULL) {
780         /* Filter out tokeninfos that don't match our token value. */
781         retval = filter_tokeninfos(context, otpvalue, tis, &filtered, &ti);
782         if (retval != 0)
783             return retval;
784
785         /* If we still don't have a single tokeninfo, choose now. */
786         if (filtered != NULL) {
787             retval = prompt_for_tokeninfo(context, prompter, prompter_data,
788                                           filtered, &ti);
789             free(filtered);
790             if (retval != 0)
791                 return retval;
792         }
793     }
794
795     assert(ti != NULL);
796
797     /* Set the value. */
798     value = make_data(strdup(otpvalue), strlen(otpvalue));
799     if (value.data == NULL)
800         return ENOMEM;
801
802     /* Collect the PIN, if necessary. */
803     retval = collect_pin(context, prompter, prompter_data, ti, &pin);
804     if (retval != 0) {
805         krb5_free_data_contents(context, &value);
806         return retval;
807     }
808
809     *out_value = value;
810     *out_pin = pin;
811     *out_ti = ti;
812     return 0;
813 }
814
815 /* Encode the OTP request into a krb5_pa_data buffer. */
816 static krb5_error_code
817 set_pa_data(const krb5_pa_otp_req *req, krb5_pa_data ***pa_data_out)
818 {
819     krb5_pa_data **out = NULL;
820     krb5_data *tmp;
821
822     /* Allocate the preauth data array and one item. */
823     out = calloc(2, sizeof(krb5_pa_data *));
824     if (out == NULL)
825         goto error;
826     out[0] = calloc(1, sizeof(krb5_pa_data));
827     out[1] = NULL;
828     if (out[0] == NULL)
829         goto error;
830
831     /* Encode our request into the preauth data item. */
832     memset(out[0], 0, sizeof(krb5_pa_data));
833     out[0]->pa_type = KRB5_PADATA_OTP_REQUEST;
834     if (encode_krb5_pa_otp_req(req, &tmp) != 0)
835         goto error;
836     out[0]->contents = (krb5_octet *)tmp->data;
837     out[0]->length = tmp->length;
838     free(tmp);
839
840     *pa_data_out = out;
841     return 0;
842
843 error:
844     if (out != NULL) {
845         free(out[0]);
846         free(out);
847     }
848     return ENOMEM;
849 }
850
851 /* Tests krb5_data to see if it is printable. */
852 static krb5_boolean
853 is_printable_string(const krb5_data *data)
854 {
855     unsigned int i;
856
857     if (data == NULL)
858         return FALSE;
859
860     for (i = 0; i < data->length; i++) {
861         if (!isprint((unsigned char)data->data[i]))
862             return FALSE;
863     }
864
865     return TRUE;
866 }
867
868 /* Returns TRUE when the given tokeninfo contains the subset of features we
869  * support. */
870 static krb5_boolean
871 is_tokeninfo_supported(krb5_otp_tokeninfo *ti)
872 {
873     krb5_flags supported_flags = KRB5_OTP_FLAG_COLLECT_PIN |
874                                  KRB5_OTP_FLAG_NO_COLLECT_PIN |
875                                  KRB5_OTP_FLAG_SEPARATE_PIN;
876
877     /* Flags we don't support... */
878     if (ti->flags & ~supported_flags)
879         return FALSE;
880
881     /* We don't currently support hashing. */
882     if (ti->supported_hash_alg != NULL || ti->iteration_count >= 0)
883         return FALSE;
884
885     /* Remove tokeninfos with invalid vendor strings. */
886     if (!is_printable_string(&ti->vendor))
887         return FALSE;
888
889     /* Remove tokeninfos with non-printable challenges. */
890     if (!is_printable_string(&ti->challenge))
891         return FALSE;
892
893     /* We don't currently support base64. */
894     if (ti->format == KRB5_OTP_FORMAT_BASE64)
895         return FALSE;
896
897     return TRUE;
898 }
899
900 /* Removes unsupported tokeninfos. Returns an error if no tokeninfos remain. */
901 static krb5_error_code
902 filter_supported_tokeninfos(krb5_context context, krb5_otp_tokeninfo **tis)
903 {
904     size_t i, j;
905
906     /* Filter out any tokeninfos we don't support. */
907     for (i = 0, j = 0; tis[i] != NULL; i++) {
908         if (!is_tokeninfo_supported(tis[i]))
909             k5_free_otp_tokeninfo(context, tis[i]);
910         else
911             tis[j++] = tis[i];
912     }
913
914     /* Terminate the array. */
915     tis[j] = NULL;
916
917     if (tis[0] != NULL)
918         return 0;
919
920     k5_setmsg(context, KRB5_PREAUTH_FAILED, _("No supported tokens"));
921     return KRB5_PREAUTH_FAILED; /* We have no supported tokeninfos. */
922 }
923
924 /*
925  * Try to find tokeninfos which match configuration data recorded in the input
926  * ccache, and if exactly one is found, drop the rest.
927  */
928 static krb5_error_code
929 filter_config_tokeninfos(krb5_context context,
930                          krb5_clpreauth_callbacks cb,
931                          krb5_clpreauth_rock rock,
932                          krb5_otp_tokeninfo **tis)
933 {
934     krb5_otp_tokeninfo *match = NULL;
935     size_t i, j;
936     const char *vendor, *alg_id, *token_id;
937
938     /* Pull up what we know about the token we want to use. */
939     vendor = cb->get_cc_config(context, rock, "vendor");
940     alg_id = cb->get_cc_config(context, rock, "algID");
941     token_id = cb->get_cc_config(context, rock, "tokenID");
942
943     /* Look for a single matching entry. */
944     for (i = 0; tis[i] != NULL; i++) {
945         if (vendor != NULL && tis[i]->vendor.length > 0 &&
946             !data_eq_string(tis[i]->vendor, vendor))
947             continue;
948         if (alg_id != NULL && tis[i]->alg_id.length > 0 &&
949             !data_eq_string(tis[i]->alg_id, alg_id))
950             continue;
951         if (token_id != NULL && tis[i]->token_id.length > 0 &&
952             !data_eq_string(tis[i]->token_id, token_id))
953             continue;
954         /* Oh, we already had a matching entry. More than one -> no change. */
955         if (match != NULL)
956             return 0;
957         match = tis[i];
958     }
959
960     /* No matching entry -> no change. */
961     if (match == NULL)
962         return 0;
963
964     /* Prune out everything except the best match. */
965     for (i = 0, j = 0; tis[i] != NULL; i++) {
966         if (tis[i] != match)
967             k5_free_otp_tokeninfo(context, tis[i]);
968         else
969             tis[j++] = tis[i];
970     }
971     tis[j] = NULL;
972
973     return 0;
974 }
975
976 static void
977 otp_client_request_init(krb5_context context, krb5_clpreauth_moddata moddata,
978                         krb5_clpreauth_modreq *modreq_out)
979 {
980     *modreq_out = calloc(1, sizeof(krb5_pa_otp_challenge *));
981 }
982
983 static krb5_error_code
984 otp_client_prep_questions(krb5_context context, krb5_clpreauth_moddata moddata,
985                           krb5_clpreauth_modreq modreq,
986                           krb5_get_init_creds_opt *opt,
987                           krb5_clpreauth_callbacks cb,
988                           krb5_clpreauth_rock rock, krb5_kdc_req *request,
989                           krb5_data *encoded_request_body,
990                           krb5_data *encoded_previous_request,
991                           krb5_pa_data *pa_data)
992 {
993     krb5_pa_otp_challenge *chl;
994     krb5_error_code retval;
995     krb5_data tmp;
996     char *json;
997
998     if (modreq == NULL)
999         return ENOMEM;
1000
1001     /* Decode the challenge. */
1002     tmp = make_data(pa_data->contents, pa_data->length);
1003     retval = decode_krb5_pa_otp_challenge(&tmp,
1004                                           (krb5_pa_otp_challenge **)modreq);
1005     if (retval != 0)
1006         return retval;
1007     chl = *(krb5_pa_otp_challenge **)modreq;
1008
1009     /* Remove unsupported tokeninfos. */
1010     retval = filter_supported_tokeninfos(context, chl->tokeninfo);
1011     if (retval != 0)
1012         return retval;
1013
1014     /* Remove tokeninfos that don't match the recorded description, if that
1015      * results in there being only one that does. */
1016     retval = filter_config_tokeninfos(context, cb, rock, chl->tokeninfo);
1017     if (retval != 0)
1018         return retval;
1019
1020     /* Make the JSON representation. */
1021     retval = codec_encode_challenge(context, chl, &json);
1022     if (retval != 0)
1023         return retval;
1024
1025     /* Ask the question. */
1026     retval = cb->ask_responder_question(context, rock,
1027                                         KRB5_RESPONDER_QUESTION_OTP,
1028                                         json);
1029     free(json);
1030     return retval;
1031 }
1032
1033 /*
1034  * Save the vendor, algID, and tokenID values for the selected token to the
1035  * out_ccache, so that later we can try to use them to select the right one
1036  * without having ot ask the user.
1037  */
1038 static void
1039 save_config_tokeninfo(krb5_context context,
1040                       krb5_clpreauth_callbacks cb,
1041                       krb5_clpreauth_rock rock,
1042                       krb5_otp_tokeninfo *ti)
1043 {
1044     char *tmp;
1045     if (ti->vendor.length > 0 &&
1046         asprintf(&tmp, "%.*s", ti->vendor.length, ti->vendor.data) >= 0) {
1047         cb->set_cc_config(context, rock, "vendor", tmp);
1048         free(tmp);
1049     }
1050     if (ti->alg_id.length > 0 &&
1051         asprintf(&tmp, "%.*s", ti->alg_id.length, ti->alg_id.data) >= 0) {
1052         cb->set_cc_config(context, rock, "algID", tmp);
1053         free(tmp);
1054     }
1055     if (ti->token_id.length > 0 &&
1056         asprintf(&tmp, "%.*s", ti->token_id.length, ti->token_id.data) >= 0) {
1057         cb->set_cc_config(context, rock, "tokenID", tmp);
1058         free(tmp);
1059     }
1060 }
1061
1062 static krb5_error_code
1063 otp_client_process(krb5_context context, krb5_clpreauth_moddata moddata,
1064                    krb5_clpreauth_modreq modreq, krb5_get_init_creds_opt *opt,
1065                    krb5_clpreauth_callbacks cb, krb5_clpreauth_rock rock,
1066                    krb5_kdc_req *request, krb5_data *encoded_request_body,
1067                    krb5_data *encoded_previous_request, krb5_pa_data *pa_data,
1068                    krb5_prompter_fct prompter, void *prompter_data,
1069                    krb5_pa_data ***pa_data_out)
1070 {
1071     krb5_pa_otp_challenge *chl = NULL;
1072     krb5_otp_tokeninfo *ti = NULL;
1073     krb5_keyblock *as_key = NULL;
1074     krb5_pa_otp_req *req = NULL;
1075     krb5_error_code retval = 0;
1076     krb5_data value, pin;
1077     const char *answer;
1078
1079     if (modreq == NULL)
1080         return ENOMEM;
1081     chl = *(krb5_pa_otp_challenge **)modreq;
1082
1083     *pa_data_out = NULL;
1084
1085     /* Get FAST armor key. */
1086     as_key = cb->fast_armor(context, rock);
1087     if (as_key == NULL)
1088         return ENOENT;
1089
1090     /* Attempt to get token selection from the responder. */
1091     pin = empty_data();
1092     value = empty_data();
1093     answer = cb->get_responder_answer(context, rock,
1094                                       KRB5_RESPONDER_QUESTION_OTP);
1095     retval = codec_decode_answer(context, answer, chl->tokeninfo, &ti, &value,
1096                                  &pin);
1097     if (retval != 0) {
1098         /* If the responder doesn't have a token selection,
1099          * we need to select the token via prompting. */
1100         retval = prompt_for_token(context, prompter, prompter_data,
1101                                   chl->tokeninfo, &ti, &value, &pin);
1102         if (retval != 0)
1103             goto error;
1104     }
1105
1106     /* Make the request. */
1107     retval = make_request(context, ti, &value, &pin, &req);
1108     if (retval != 0)
1109         goto error;
1110
1111     /* Save information about the token which was used. */
1112     save_config_tokeninfo(context, cb, rock, ti);
1113
1114     /* Encrypt the challenge's nonce and set it in the request. */
1115     retval = encrypt_nonce(context, as_key, chl, req);
1116     if (retval != 0)
1117         goto error;
1118
1119     /* Use FAST armor key as response key. */
1120     retval = cb->set_as_key(context, rock, as_key);
1121     if (retval != 0)
1122         goto error;
1123
1124     /* Encode the request into the pa_data output. */
1125     retval = set_pa_data(req, pa_data_out);
1126 error:
1127     krb5_free_data_contents(context, &value);
1128     krb5_free_data_contents(context, &pin);
1129     k5_free_pa_otp_req(context, req);
1130     return retval;
1131 }
1132
1133 static void
1134 otp_client_request_fini(krb5_context context, krb5_clpreauth_moddata moddata,
1135                         krb5_clpreauth_modreq modreq)
1136 {
1137     if (modreq == NULL)
1138         return;
1139
1140     k5_free_pa_otp_challenge(context, *(krb5_pa_otp_challenge **)modreq);
1141     free(modreq);
1142 }
1143
1144 krb5_error_code
1145 clpreauth_otp_initvt(krb5_context context, int maj_ver, int min_ver,
1146                      krb5_plugin_vtable vtable)
1147 {
1148     krb5_clpreauth_vtable vt;
1149
1150     if (maj_ver != 1)
1151         return KRB5_PLUGIN_VER_NOTSUPP;
1152
1153     vt = (krb5_clpreauth_vtable)vtable;
1154     vt->name = "otp";
1155     vt->pa_type_list = otp_client_supported_pa_types;
1156     vt->request_init = otp_client_request_init;
1157     vt->prep_questions = otp_client_prep_questions;
1158     vt->process = otp_client_process;
1159     vt->request_fini = otp_client_request_fini;
1160     vt->gic_opts = NULL;
1161
1162     return 0;
1163 }
1164
1165 krb5_error_code KRB5_CALLCONV
1166 krb5_responder_otp_get_challenge(krb5_context ctx,
1167                                  krb5_responder_context rctx,
1168                                  krb5_responder_otp_challenge **chl)
1169 {
1170     const char *answer;
1171     krb5_responder_otp_challenge *challenge;
1172
1173     answer = krb5_responder_get_challenge(ctx, rctx,
1174                                           KRB5_RESPONDER_QUESTION_OTP);
1175     if (answer == NULL) {
1176         *chl = NULL;
1177         return 0;
1178     }
1179
1180     challenge = codec_decode_challenge(ctx, answer);
1181     if (challenge == NULL)
1182         return ENOMEM;
1183
1184     *chl = challenge;
1185     return 0;
1186 }
1187
1188 krb5_error_code KRB5_CALLCONV
1189 krb5_responder_otp_set_answer(krb5_context ctx, krb5_responder_context rctx,
1190                               size_t ti, const char *value, const char *pin)
1191 {
1192     krb5_error_code retval;
1193     k5_json_object obj = NULL;
1194     k5_json_number num;
1195     k5_json_string str;
1196     char *tmp;
1197
1198     retval = k5_json_object_create(&obj);
1199     if (retval != 0)
1200         goto error;
1201
1202     retval = k5_json_number_create(ti, &num);
1203     if (retval != 0)
1204         goto error;
1205
1206     retval = k5_json_object_set(obj, "tokeninfo", num);
1207     k5_json_release(num);
1208     if (retval != 0)
1209         goto error;
1210
1211     if (value != NULL) {
1212         retval = k5_json_string_create(value, &str);
1213         if (retval != 0)
1214             goto error;
1215
1216         retval = k5_json_object_set(obj, "value", str);
1217         k5_json_release(str);
1218         if (retval != 0)
1219             goto error;
1220     }
1221
1222     if (pin != NULL) {
1223         retval = k5_json_string_create(pin, &str);
1224         if (retval != 0)
1225             goto error;
1226
1227         retval = k5_json_object_set(obj, "pin", str);
1228         k5_json_release(str);
1229         if (retval != 0)
1230             goto error;
1231     }
1232
1233     retval = k5_json_encode(obj, &tmp);
1234     if (retval != 0)
1235         goto error;
1236     k5_json_release(obj);
1237
1238     retval = krb5_responder_set_answer(ctx, rctx, KRB5_RESPONDER_QUESTION_OTP,
1239                                        tmp);
1240     free(tmp);
1241     return retval;
1242
1243 error:
1244     k5_json_release(obj);
1245     return retval;
1246 }
1247
1248 void KRB5_CALLCONV
1249 krb5_responder_otp_challenge_free(krb5_context ctx,
1250                                   krb5_responder_context rctx,
1251                                   krb5_responder_otp_challenge *chl)
1252 {
1253     size_t i;
1254
1255     if (chl == NULL)
1256         return;
1257
1258     for (i = 0; chl->tokeninfo[i]; i++)
1259         free_tokeninfo(chl->tokeninfo[i]);
1260     free(chl->service);
1261     free(chl->tokeninfo);
1262     free(chl);
1263 }