anastasis_authorization_plugin_sms.c (20368B)
1 /* 2 This file is part of Anastasis 3 Copyright (C) 2019, 2021 Anastasis SARL 4 5 Anastasis is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file anastasis_authorization_plugin_sms.c 18 * @brief authorization plugin email based 19 * @author Dominik Meister 20 */ 21 #include "platform.h" 22 #include "anastasis_authorization_plugin.h" 23 #include <taler/taler_mhd_lib.h> 24 #include <taler/taler_json_lib.h> 25 #include <regex.h> 26 #include "anastasis_util_lib.h" 27 #include <gnunet/gnunet_db_lib.h> 28 #include "anastasis_database_lib.h" 29 30 /** 31 * How many retries do we allow per code? 32 */ 33 #define INITIAL_RETRY_COUNTER 3 34 35 /** 36 * Saves the State of a authorization plugin. 37 */ 38 struct SMS_Context 39 { 40 41 /** 42 * Command which is executed to run the plugin (some bash script or a 43 * command line argument) 44 */ 45 char *auth_command; 46 47 /** 48 * Regex for phone number validation. 49 */ 50 regex_t regex; 51 52 /** 53 * Messages of the plugin, read from a resource file. 54 */ 55 json_t *messages; 56 57 /** 58 * Context we operate in. 59 */ 60 const struct ANASTASIS_AuthorizationContext *ac; 61 }; 62 63 64 /** 65 * Saves the State of a authorization process 66 */ 67 struct ANASTASIS_AUTHORIZATION_State 68 { 69 /** 70 * Public key of the challenge which is authorised 71 */ 72 struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; 73 74 /** 75 * Code which is sent to the user (here sent via SMS) 76 */ 77 uint64_t code; 78 79 /** 80 * Our plugin context. 81 */ 82 struct SMS_Context *ctx; 83 84 /** 85 * Function to call when we made progress. 86 */ 87 GNUNET_SCHEDULER_TaskCallback trigger; 88 89 /** 90 * Closure for @e trigger. 91 */ 92 void *trigger_cls; 93 94 /** 95 * holds the truth information 96 */ 97 char *phone_number; 98 99 /** 100 * Handle to the helper process. 101 */ 102 struct GNUNET_Process *child; 103 104 /** 105 * Handle to wait for @e child 106 */ 107 struct GNUNET_ChildWaitHandle *cwh; 108 109 /** 110 * Our client connection, set if suspended. 111 */ 112 struct MHD_Connection *connection; 113 114 /** 115 * Message to send. 116 */ 117 char *msg; 118 119 /** 120 * Offset of transmission in msg. 121 */ 122 size_t msg_off; 123 124 /** 125 * Exit code from helper. 126 */ 127 long unsigned int exit_code; 128 129 /** 130 * How did the helper die? 131 */ 132 enum GNUNET_OS_ProcessStatusType pst; 133 134 }; 135 136 137 /** 138 * Returned by #get_message() when the configured messages file has no entry 139 * for the requested ID. This is an installation error, but it must not be 140 * allowed to reach GNUNET_asprintf() as a NULL format string. The text 141 * deliberately contains no printf conversions, so substituting it for any 142 * template is safe whatever argument list the caller passes. 143 */ 144 #define MISSING_MESSAGE \ 145 "The provider is misconfigured: a message template is missing." 146 147 148 /** 149 * Obtain internationalized message @a msg_id from @a ctx using 150 * language preferences of @a conn. 151 * 152 * @param messages JSON object to lookup message from 153 * @param conn connection to lookup message for 154 * @param msg_id unique message ID 155 * @return the requested message, or #MISSING_MESSAGE if it was not 156 * configured; never NULL 157 */ 158 static const char * 159 get_message (const json_t *messages, 160 struct MHD_Connection *conn, 161 const char *msg_id) 162 { 163 const char *accept_lang; 164 165 accept_lang = MHD_lookup_connection_value (conn, 166 MHD_HEADER_KIND, 167 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 168 if (NULL == accept_lang) 169 accept_lang = "en_US"; 170 { 171 const char *ret; 172 struct GNUNET_JSON_Specification spec[] = { 173 TALER_JSON_spec_i18n_string (msg_id, 174 accept_lang, 175 &ret), 176 GNUNET_JSON_spec_end () 177 }; 178 179 if (GNUNET_OK != 180 GNUNET_JSON_parse (messages, 181 spec, 182 NULL, NULL)) 183 { 184 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 185 "Message `%s' is missing from the configured messages file\n", 186 msg_id); 187 GNUNET_JSON_parse_free (spec); 188 return MISSING_MESSAGE; 189 } 190 GNUNET_JSON_parse_free (spec); 191 if (NULL == ret) 192 { 193 /* The parser of TALER_JSON_spec_i18n_string returns #GNUNET_OK even 194 when the field is absent or is not a string, in which case it stores 195 NULL; the check above therefore never fires for a missing message and 196 this one is what keeps NULL out of the format argument of the 197 GNUNET_asprintf() calls below. */ 198 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 199 "Message `%s' is missing or not a string in the configured messages file\n", 200 msg_id); 201 return MISSING_MESSAGE; 202 } 203 return ret; 204 } 205 } 206 207 208 /** 209 * Validate @a data is a well-formed input into the challenge method, 210 * i.e. @a data is a well-formed phone number for sending an SMS, or 211 * a well-formed e-mail address for sending an e-mail. Not expected to 212 * check that the phone number or e-mail account actually exists. 213 * 214 * To be possibly used before issuing a 402 payment required to the client. 215 * 216 * @param cls closure with a `struct SMS_Context` 217 * @param connection HTTP client request (for queuing response) 218 * @param truth_mime mime type of @e data 219 * @param data input to validate (i.e. is it a valid phone number, etc.) 220 * @param data_length number of bytes in @a data 221 * @return #GNUNET_OK if @a data is valid, 222 * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection 223 * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection 224 */ 225 static enum GNUNET_GenericReturnValue 226 sms_validate (void *cls, 227 struct MHD_Connection *connection, 228 const char *truth_mime, 229 const char *data, 230 size_t data_length) 231 { 232 struct SMS_Context *ctx = cls; 233 int regex_result; 234 char *phone_number; 235 236 phone_number = GNUNET_strndup (data, 237 data_length); 238 regex_result = regexec (&ctx->regex, 239 phone_number, 240 0, 241 NULL, 242 0); 243 GNUNET_free (phone_number); 244 if (0 != regex_result) 245 { 246 if (MHD_NO == 247 TALER_MHD_reply_with_error (connection, 248 MHD_HTTP_CONFLICT, 249 TALER_EC_ANASTASIS_SMS_PHONE_INVALID, 250 NULL)) 251 return GNUNET_SYSERR; 252 return GNUNET_NO; 253 } 254 return GNUNET_OK; 255 } 256 257 258 /** 259 * Begin issuing authentication challenge to user based on @a data. 260 * Sends SMS. 261 * 262 * @param cls closure with a `struct SMS_Context` 263 * @param trigger function to call when we made progress 264 * @param trigger_cls closure for @a trigger 265 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the 266 * interaction with the user 267 * @param code secret code that the user has to provide back to satisfy the challenge in 268 * the main anastasis protocol 269 * @param data input to validate (i.e. is it a valid phone number, etc.) 270 * @param data_length number of bytes in @a data 271 * @return state to track progress on the authorization operation, NULL on failure 272 */ 273 static struct ANASTASIS_AUTHORIZATION_State * 274 sms_start (void *cls, 275 GNUNET_SCHEDULER_TaskCallback trigger, 276 void *trigger_cls, 277 const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, 278 uint64_t code, 279 const void *data, 280 size_t data_length) 281 { 282 struct SMS_Context *ctx = cls; 283 struct ANASTASIS_AUTHORIZATION_State *as; 284 enum GNUNET_DB_QueryStatus qs; 285 286 /* If the user can show this challenge code, this 287 plugin is already happy (no additional 288 requirements), so mark this challenge as 289 already satisfied from the start. */ 290 qs = ANASTASIS_DB_update_to_challenge_code_satisfied ( 291 truth_uuid, 292 code); 293 if (qs <= 0) 294 { 295 GNUNET_break (0); 296 return NULL; 297 } 298 as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); 299 as->trigger = trigger; 300 as->trigger_cls = trigger_cls; 301 as->ctx = ctx; 302 as->truth_uuid = *truth_uuid; 303 as->code = code; 304 as->phone_number = GNUNET_strndup (data, 305 data_length); 306 return as; 307 } 308 309 310 /** 311 * Function called when our SMS helper has terminated. 312 * 313 * @param cls our `struct ANASTASIS_AUHTORIZATION_State` 314 * @param type type of the process 315 * @param exit_code status code of the process 316 */ 317 static void 318 sms_done_cb (void *cls, 319 enum GNUNET_OS_ProcessStatusType type, 320 long unsigned int exit_code) 321 { 322 struct ANASTASIS_AUTHORIZATION_State *as = cls; 323 324 as->cwh = NULL; 325 if (NULL != as->child) 326 { 327 GNUNET_process_destroy (as->child); 328 as->child = NULL; 329 } 330 as->pst = type; 331 as->exit_code = exit_code; 332 MHD_resume_connection (as->connection); 333 as->trigger (as->trigger_cls); 334 } 335 336 337 /** 338 * Begin issuing authentication challenge to user based on @a data. 339 * I.e. start to send SMS or e-mail or launch video identification. 340 * 341 * @param as authorization state 342 * @param connection HTTP client request (for queuing response, such as redirection to video portal) 343 * @return state of the request 344 */ 345 static enum ANASTASIS_AUTHORIZATION_ChallengeResult 346 sms_challenge (struct ANASTASIS_AUTHORIZATION_State *as, 347 struct MHD_Connection *connection) 348 { 349 enum MHD_Result mres; 350 const char *mime; 351 const char *lang; 352 353 mime = MHD_lookup_connection_value (connection, 354 MHD_HEADER_KIND, 355 MHD_HTTP_HEADER_ACCEPT); 356 if (NULL == mime) 357 mime = "text/plain"; 358 lang = MHD_lookup_connection_value (connection, 359 MHD_HEADER_KIND, 360 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 361 if (NULL == lang) 362 lang = "en"; 363 if (NULL == as->msg) 364 { 365 /* First time, start child process and feed pipe */ 366 struct GNUNET_DISK_PipeHandle *p; 367 struct GNUNET_DISK_FileHandle *pipe_stdin; 368 369 p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); 370 if (NULL == p) 371 { 372 mres = TALER_MHD_reply_with_error (connection, 373 MHD_HTTP_INTERNAL_SERVER_ERROR, 374 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, 375 "pipe"); 376 if (MHD_YES != mres) 377 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 378 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 379 } 380 as->child = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 381 GNUNET_assert (GNUNET_OK == 382 GNUNET_process_set_options ( 383 as->child, 384 GNUNET_process_option_inherit_rpipe (p, 385 STDIN_FILENO))); 386 if (GNUNET_OK != 387 GNUNET_process_run_command_va (as->child, 388 as->ctx->auth_command, 389 as->ctx->auth_command, 390 as->phone_number, 391 NULL)) 392 { 393 GNUNET_process_destroy (as->child); 394 as->child = NULL; 395 GNUNET_DISK_pipe_close (p); 396 mres = TALER_MHD_reply_with_error (connection, 397 MHD_HTTP_INTERNAL_SERVER_ERROR, 398 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, 399 "exec"); 400 if (MHD_YES != mres) 401 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 402 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 403 } 404 pipe_stdin = GNUNET_DISK_pipe_detach_end (p, 405 GNUNET_DISK_PIPE_END_WRITE); 406 GNUNET_assert (NULL != pipe_stdin); 407 GNUNET_DISK_pipe_close (p); 408 GNUNET_asprintf (&as->msg, 409 "%s\nAnastasis:\n%s", 410 ANASTASIS_pin2s (as->code), 411 ANASTASIS_CRYPTO_uuid2s (&as->truth_uuid)); 412 { 413 const char *off = as->msg; 414 size_t left = strlen (off); 415 416 while (0 != left) 417 { 418 ssize_t ret; 419 420 ret = GNUNET_DISK_file_write (pipe_stdin, 421 off, 422 left); 423 if (ret <= 0) 424 { 425 mres = TALER_MHD_reply_with_error (connection, 426 MHD_HTTP_INTERNAL_SERVER_ERROR, 427 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, 428 "write"); 429 if (MHD_YES != mres) 430 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 431 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 432 } 433 as->msg_off += ret; 434 off += ret; 435 left -= ret; 436 } 437 GNUNET_DISK_file_close (pipe_stdin); 438 } 439 as->cwh = GNUNET_wait_child (as->child, 440 &sms_done_cb, 441 as); 442 as->connection = connection; 443 MHD_suspend_connection (connection); 444 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 445 } 446 if (NULL != as->cwh) 447 { 448 /* Spurious call, why are we here? */ 449 GNUNET_break (0); 450 MHD_suspend_connection (connection); 451 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 452 } 453 if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || 454 (0 != as->exit_code) ) 455 { 456 char es[32]; 457 458 GNUNET_snprintf (es, 459 sizeof (es), 460 "%u/%d", 461 (unsigned int) as->exit_code, 462 as->pst); 463 mres = TALER_MHD_reply_with_error (connection, 464 MHD_HTTP_INTERNAL_SERVER_ERROR, 465 TALER_EC_ANASTASIS_SMS_HELPER_COMMAND_FAILED, 466 es); 467 if (MHD_YES != mres) 468 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 469 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 470 } 471 472 /* Build HTTP response */ 473 { 474 struct MHD_Response *resp; 475 const char *end; 476 size_t slen; 477 478 slen = strlen (as->phone_number); 479 if (slen > 4) 480 end = &as->phone_number[slen - 4]; 481 else 482 end = &as->phone_number[slen / 2]; 483 484 if (0.0 < TALER_pattern_matches (mime, 485 "application/json")) 486 { 487 resp = TALER_MHD_MAKE_JSON_PACK ( 488 GNUNET_JSON_pack_string ("challenge_type", 489 "TAN_SENT"), 490 GNUNET_JSON_pack_string ("tan_address_hint", 491 end)); 492 } 493 else 494 { 495 size_t reply_len; 496 char *reply; 497 498 reply_len = GNUNET_asprintf (&reply, 499 get_message (as->ctx->messages, 500 connection, 501 "instructions"), 502 end); 503 resp = MHD_create_response_from_buffer (reply_len, 504 reply, 505 MHD_RESPMEM_MUST_COPY); 506 GNUNET_free (reply); 507 TALER_MHD_add_global_headers (resp, 508 false); 509 GNUNET_break (MHD_YES == 510 MHD_add_response_header (resp, 511 MHD_HTTP_HEADER_CONTENT_TYPE, 512 "text/plain")); 513 } 514 mres = MHD_queue_response (connection, 515 MHD_HTTP_OK, 516 resp); 517 MHD_destroy_response (resp); 518 if (MHD_YES != mres) 519 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED; 520 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS; 521 } 522 } 523 524 525 /** 526 * Free internal state associated with @a as. 527 * 528 * @param as state to clean up 529 */ 530 static void 531 sms_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) 532 { 533 if (NULL != as->cwh) 534 { 535 GNUNET_wait_child_cancel (as->cwh); 536 as->cwh = NULL; 537 } 538 if (NULL != as->child) 539 { 540 GNUNET_break (GNUNET_OK == 541 GNUNET_process_kill (as->child, 542 SIGKILL)); 543 GNUNET_break (GNUNET_OK == 544 GNUNET_process_wait (as->child, 545 true, 546 NULL, 547 NULL)); 548 GNUNET_process_destroy (as->child); 549 as->child = NULL; 550 } 551 GNUNET_free (as->msg); 552 GNUNET_free (as->phone_number); 553 GNUNET_free (as); 554 } 555 556 557 /** 558 * Initialize email based authorization plugin 559 * 560 * @param cls a configuration instance 561 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` 562 */ 563 void * 564 libanastasis_plugin_authorization_sms_init (void *cls); 565 566 /* declaration to fix compiler warning */ 567 void * 568 libanastasis_plugin_authorization_sms_init (void *cls) 569 { 570 const struct ANASTASIS_AuthorizationContext *ac = cls; 571 struct ANASTASIS_AuthorizationPlugin *plugin; 572 const struct GNUNET_CONFIGURATION_Handle *cfg = ac->cfg; 573 struct SMS_Context *ctx; 574 575 ctx = GNUNET_new (struct SMS_Context); 576 ctx->ac = ac; 577 { 578 char *fn; 579 json_error_t err; 580 char *tmp; 581 582 tmp = GNUNET_OS_installation_get_path (ANASTASIS_project_data (), 583 GNUNET_OS_IPK_DATADIR); 584 GNUNET_asprintf (&fn, 585 "%sauthorization-sms-messages.json", 586 tmp); 587 GNUNET_free (tmp); 588 ctx->messages = json_load_file (fn, 589 JSON_REJECT_DUPLICATES, 590 &err); 591 if (NULL == ctx->messages) 592 { 593 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 594 "Failed to load messages from `%s': %s at %d:%d\n", 595 fn, 596 err.text, 597 err.line, 598 err.column); 599 GNUNET_free (fn); 600 GNUNET_free (ctx); 601 return NULL; 602 } 603 GNUNET_free (fn); 604 } 605 { 606 int regex_result; 607 const char *regexp = "^\\+?[0-9]+$"; 608 609 regex_result = regcomp (&ctx->regex, 610 regexp, 611 REG_EXTENDED); 612 if (0 != regex_result) 613 { 614 GNUNET_break (0); 615 json_decref (ctx->messages); 616 GNUNET_free (ctx); 617 return NULL; 618 } 619 } 620 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); 621 plugin->retry_counter = INITIAL_RETRY_COUNTER; 622 plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS; 623 plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS; 624 plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; 625 plugin->cls = ctx; 626 plugin->validate = &sms_validate; 627 plugin->start = &sms_start; 628 plugin->challenge = &sms_challenge; 629 plugin->cleanup = &sms_cleanup; 630 631 if (GNUNET_OK != 632 GNUNET_CONFIGURATION_get_value_string (cfg, 633 "authorization-sms", 634 "COMMAND", 635 &ctx->auth_command)) 636 { 637 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 638 "authorization-sms", 639 "COMMAND"); 640 regfree (&ctx->regex); 641 json_decref (ctx->messages); 642 GNUNET_free (ctx); 643 GNUNET_free (plugin); 644 return NULL; 645 } 646 return plugin; 647 } 648 649 650 /** 651 * Unload authorization plugin 652 * 653 * @param cls a `struct ANASTASIS_AuthorizationPlugin` 654 * @return NULL (always) 655 */ 656 void * 657 libanastasis_plugin_authorization_sms_done (void *cls); 658 659 /* declaration to fix compiler warning */ 660 void * 661 libanastasis_plugin_authorization_sms_done (void *cls) 662 { 663 struct ANASTASIS_AuthorizationPlugin *plugin = cls; 664 struct SMS_Context *ctx = plugin->cls; 665 666 GNUNET_free (ctx->auth_command); 667 regfree (&ctx->regex); 668 json_decref (ctx->messages); 669 GNUNET_free (ctx); 670 GNUNET_free (plugin); 671 return NULL; 672 }