anastasis_authorization_plugin_sms.c (19225B)
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 * Obtain internationalized message @a msg_id from @a ctx using 139 * language preferences of @a conn. 140 * 141 * @param messages JSON object to lookup message from 142 * @param conn connection to lookup message for 143 * @param msg_id unique message ID 144 * @return NULL if message was not found 145 */ 146 static const char * 147 get_message (const json_t *messages, 148 struct MHD_Connection *conn, 149 const char *msg_id) 150 { 151 const char *accept_lang; 152 153 accept_lang = MHD_lookup_connection_value (conn, 154 MHD_HEADER_KIND, 155 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 156 if (NULL == accept_lang) 157 accept_lang = "en_US"; 158 { 159 const char *ret; 160 struct GNUNET_JSON_Specification spec[] = { 161 TALER_JSON_spec_i18n_string (msg_id, 162 accept_lang, 163 &ret), 164 GNUNET_JSON_spec_end () 165 }; 166 167 if (GNUNET_OK != 168 GNUNET_JSON_parse (messages, 169 spec, 170 NULL, NULL)) 171 { 172 GNUNET_break (0); 173 GNUNET_JSON_parse_free (spec); 174 return NULL; 175 } 176 GNUNET_JSON_parse_free (spec); 177 return ret; 178 } 179 } 180 181 182 /** 183 * Validate @a data is a well-formed input into the challenge method, 184 * i.e. @a data is a well-formed phone number for sending an SMS, or 185 * a well-formed e-mail address for sending an e-mail. Not expected to 186 * check that the phone number or e-mail account actually exists. 187 * 188 * To be possibly used before issuing a 402 payment required to the client. 189 * 190 * @param cls closure with a `struct SMS_Context` 191 * @param connection HTTP client request (for queuing response) 192 * @param truth_mime mime type of @e data 193 * @param data input to validate (i.e. is it a valid phone number, etc.) 194 * @param data_length number of bytes in @a data 195 * @return #GNUNET_OK if @a data is valid, 196 * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection 197 * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection 198 */ 199 static enum GNUNET_GenericReturnValue 200 sms_validate (void *cls, 201 struct MHD_Connection *connection, 202 const char *truth_mime, 203 const char *data, 204 size_t data_length) 205 { 206 struct SMS_Context *ctx = cls; 207 int regex_result; 208 char *phone_number; 209 210 phone_number = GNUNET_strndup (data, 211 data_length); 212 regex_result = regexec (&ctx->regex, 213 phone_number, 214 0, 215 NULL, 216 0); 217 GNUNET_free (phone_number); 218 if (0 != regex_result) 219 { 220 if (MHD_NO == 221 TALER_MHD_reply_with_error (connection, 222 MHD_HTTP_CONFLICT, 223 TALER_EC_ANASTASIS_SMS_PHONE_INVALID, 224 NULL)) 225 return GNUNET_SYSERR; 226 return GNUNET_NO; 227 } 228 return GNUNET_OK; 229 } 230 231 232 /** 233 * Begin issuing authentication challenge to user based on @a data. 234 * Sends SMS. 235 * 236 * @param cls closure with a `struct SMS_Context` 237 * @param trigger function to call when we made progress 238 * @param trigger_cls closure for @a trigger 239 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the 240 * interaction with the user 241 * @param code secret code that the user has to provide back to satisfy the challenge in 242 * the main anastasis protocol 243 * @param data input to validate (i.e. is it a valid phone number, etc.) 244 * @param data_length number of bytes in @a data 245 * @return state to track progress on the authorization operation, NULL on failure 246 */ 247 static struct ANASTASIS_AUTHORIZATION_State * 248 sms_start (void *cls, 249 GNUNET_SCHEDULER_TaskCallback trigger, 250 void *trigger_cls, 251 const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, 252 uint64_t code, 253 const void *data, 254 size_t data_length) 255 { 256 struct SMS_Context *ctx = cls; 257 struct ANASTASIS_AUTHORIZATION_State *as; 258 enum GNUNET_DB_QueryStatus qs; 259 260 /* If the user can show this challenge code, this 261 plugin is already happy (no additional 262 requirements), so mark this challenge as 263 already satisfied from the start. */ 264 qs = ctx->ac->db->mark_challenge_code_satisfied (ctx->ac->db->cls, 265 truth_uuid, 266 code); 267 if (qs <= 0) 268 { 269 GNUNET_break (0); 270 return NULL; 271 } 272 as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); 273 as->trigger = trigger; 274 as->trigger_cls = trigger_cls; 275 as->ctx = ctx; 276 as->truth_uuid = *truth_uuid; 277 as->code = code; 278 as->phone_number = GNUNET_strndup (data, 279 data_length); 280 return as; 281 } 282 283 284 /** 285 * Function called when our SMS helper has terminated. 286 * 287 * @param cls our `struct ANASTASIS_AUHTORIZATION_State` 288 * @param type type of the process 289 * @param exit_code status code of the process 290 */ 291 static void 292 sms_done_cb (void *cls, 293 enum GNUNET_OS_ProcessStatusType type, 294 long unsigned int exit_code) 295 { 296 struct ANASTASIS_AUTHORIZATION_State *as = cls; 297 298 as->cwh = NULL; 299 if (NULL != as->child) 300 { 301 GNUNET_process_destroy (as->child); 302 as->child = NULL; 303 } 304 as->pst = type; 305 as->exit_code = exit_code; 306 MHD_resume_connection (as->connection); 307 as->trigger (as->trigger_cls); 308 } 309 310 311 /** 312 * Begin issuing authentication challenge to user based on @a data. 313 * I.e. start to send SMS or e-mail or launch video identification. 314 * 315 * @param as authorization state 316 * @param connection HTTP client request (for queuing response, such as redirection to video portal) 317 * @return state of the request 318 */ 319 static enum ANASTASIS_AUTHORIZATION_ChallengeResult 320 sms_challenge (struct ANASTASIS_AUTHORIZATION_State *as, 321 struct MHD_Connection *connection) 322 { 323 MHD_RESULT mres; 324 const char *mime; 325 const char *lang; 326 327 mime = MHD_lookup_connection_value (connection, 328 MHD_HEADER_KIND, 329 MHD_HTTP_HEADER_ACCEPT); 330 if (NULL == mime) 331 mime = "text/plain"; 332 lang = MHD_lookup_connection_value (connection, 333 MHD_HEADER_KIND, 334 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 335 if (NULL == lang) 336 lang = "en"; 337 if (NULL == as->msg) 338 { 339 /* First time, start child process and feed pipe */ 340 struct GNUNET_DISK_PipeHandle *p; 341 struct GNUNET_DISK_FileHandle *pipe_stdin; 342 343 p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); 344 if (NULL == p) 345 { 346 mres = TALER_MHD_reply_with_error (connection, 347 MHD_HTTP_INTERNAL_SERVER_ERROR, 348 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, 349 "pipe"); 350 if (MHD_YES != mres) 351 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 352 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 353 } 354 as->child = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 355 GNUNET_assert (GNUNET_OK == 356 GNUNET_process_set_options ( 357 as->child, 358 GNUNET_process_option_inherit_rpipe (p, 359 STDIN_FILENO))); 360 if (GNUNET_OK != 361 GNUNET_process_run_command_va (as->child, 362 as->ctx->auth_command, 363 as->ctx->auth_command, 364 as->phone_number, 365 NULL)) 366 { 367 GNUNET_process_destroy (as->child); 368 as->child = NULL; 369 GNUNET_DISK_pipe_close (p); 370 mres = TALER_MHD_reply_with_error (connection, 371 MHD_HTTP_INTERNAL_SERVER_ERROR, 372 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, 373 "exec"); 374 if (MHD_YES != mres) 375 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 376 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 377 } 378 pipe_stdin = GNUNET_DISK_pipe_detach_end (p, 379 GNUNET_DISK_PIPE_END_WRITE); 380 GNUNET_assert (NULL != pipe_stdin); 381 GNUNET_DISK_pipe_close (p); 382 GNUNET_asprintf (&as->msg, 383 "%s\nAnastasis:\n%s", 384 ANASTASIS_pin2s (as->code), 385 ANASTASIS_CRYPTO_uuid2s (&as->truth_uuid)); 386 { 387 const char *off = as->msg; 388 size_t left = strlen (off); 389 390 while (0 != left) 391 { 392 ssize_t ret; 393 394 ret = GNUNET_DISK_file_write (pipe_stdin, 395 off, 396 left); 397 if (ret <= 0) 398 { 399 mres = TALER_MHD_reply_with_error (connection, 400 MHD_HTTP_INTERNAL_SERVER_ERROR, 401 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, 402 "write"); 403 if (MHD_YES != mres) 404 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 405 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 406 } 407 as->msg_off += ret; 408 off += ret; 409 left -= ret; 410 } 411 GNUNET_DISK_file_close (pipe_stdin); 412 } 413 as->cwh = GNUNET_wait_child (as->child, 414 &sms_done_cb, 415 as); 416 as->connection = connection; 417 MHD_suspend_connection (connection); 418 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 419 } 420 if (NULL != as->cwh) 421 { 422 /* Spurious call, why are we here? */ 423 GNUNET_break (0); 424 MHD_suspend_connection (connection); 425 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 426 } 427 if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || 428 (0 != as->exit_code) ) 429 { 430 char es[32]; 431 432 GNUNET_snprintf (es, 433 sizeof (es), 434 "%u/%d", 435 (unsigned int) as->exit_code, 436 as->pst); 437 mres = TALER_MHD_reply_with_error (connection, 438 MHD_HTTP_INTERNAL_SERVER_ERROR, 439 TALER_EC_ANASTASIS_SMS_HELPER_COMMAND_FAILED, 440 es); 441 if (MHD_YES != mres) 442 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 443 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 444 } 445 446 /* Build HTTP response */ 447 { 448 struct MHD_Response *resp; 449 const char *end; 450 size_t slen; 451 452 slen = strlen (as->phone_number); 453 if (slen > 4) 454 end = &as->phone_number[slen - 4]; 455 else 456 end = &as->phone_number[slen / 2]; 457 458 if (0.0 < TALER_pattern_matches (mime, 459 "application/json")) 460 { 461 resp = TALER_MHD_MAKE_JSON_PACK ( 462 GNUNET_JSON_pack_string ("challenge_type", 463 "TAN_SENT"), 464 GNUNET_JSON_pack_string ("tan_address_hint", 465 end)); 466 } 467 else 468 { 469 size_t reply_len; 470 char *reply; 471 472 reply_len = GNUNET_asprintf (&reply, 473 get_message (as->ctx->messages, 474 connection, 475 "instructions"), 476 end); 477 resp = MHD_create_response_from_buffer (reply_len, 478 reply, 479 MHD_RESPMEM_MUST_COPY); 480 GNUNET_free (reply); 481 TALER_MHD_add_global_headers (resp, 482 false); 483 GNUNET_break (MHD_YES == 484 MHD_add_response_header (resp, 485 MHD_HTTP_HEADER_CONTENT_TYPE, 486 "text/plain")); 487 } 488 mres = MHD_queue_response (connection, 489 MHD_HTTP_OK, 490 resp); 491 MHD_destroy_response (resp); 492 if (MHD_YES != mres) 493 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED; 494 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS; 495 } 496 } 497 498 499 /** 500 * Free internal state associated with @a as. 501 * 502 * @param as state to clean up 503 */ 504 static void 505 sms_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) 506 { 507 if (NULL != as->cwh) 508 { 509 GNUNET_wait_child_cancel (as->cwh); 510 as->cwh = NULL; 511 } 512 if (NULL != as->child) 513 { 514 GNUNET_break (GNUNET_OK == 515 GNUNET_process_kill (as->child, 516 SIGKILL)); 517 GNUNET_break (GNUNET_OK == 518 GNUNET_process_wait (as->child, 519 true, 520 NULL, 521 NULL)); 522 GNUNET_process_destroy (as->child); 523 as->child = NULL; 524 } 525 GNUNET_free (as->msg); 526 GNUNET_free (as->phone_number); 527 GNUNET_free (as); 528 } 529 530 531 /** 532 * Initialize email based authorization plugin 533 * 534 * @param cls a configuration instance 535 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` 536 */ 537 void * 538 libanastasis_plugin_authorization_sms_init (void *cls); 539 540 /* declaration to fix compiler warning */ 541 void * 542 libanastasis_plugin_authorization_sms_init (void *cls) 543 { 544 const struct ANASTASIS_AuthorizationContext *ac = cls; 545 struct ANASTASIS_AuthorizationPlugin *plugin; 546 const struct GNUNET_CONFIGURATION_Handle *cfg = ac->cfg; 547 struct SMS_Context *ctx; 548 549 ctx = GNUNET_new (struct SMS_Context); 550 ctx->ac = ac; 551 { 552 char *fn; 553 json_error_t err; 554 char *tmp; 555 556 tmp = GNUNET_OS_installation_get_path (ANASTASIS_project_data (), 557 GNUNET_OS_IPK_DATADIR); 558 GNUNET_asprintf (&fn, 559 "%sauthorization-sms-messages.json", 560 tmp); 561 GNUNET_free (tmp); 562 ctx->messages = json_load_file (fn, 563 JSON_REJECT_DUPLICATES, 564 &err); 565 if (NULL == ctx->messages) 566 { 567 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 568 "Failed to load messages from `%s': %s at %d:%d\n", 569 fn, 570 err.text, 571 err.line, 572 err.column); 573 GNUNET_free (fn); 574 GNUNET_free (ctx); 575 return NULL; 576 } 577 GNUNET_free (fn); 578 } 579 { 580 int regex_result; 581 const char *regexp = "^\\+?[0-9]+$"; 582 583 regex_result = regcomp (&ctx->regex, 584 regexp, 585 REG_EXTENDED); 586 if (0 != regex_result) 587 { 588 GNUNET_break (0); 589 json_decref (ctx->messages); 590 GNUNET_free (ctx); 591 return NULL; 592 } 593 } 594 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); 595 plugin->retry_counter = INITIAL_RETRY_COUNTER; 596 plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS; 597 plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS; 598 plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; 599 plugin->cls = ctx; 600 plugin->validate = &sms_validate; 601 plugin->start = &sms_start; 602 plugin->challenge = &sms_challenge; 603 plugin->cleanup = &sms_cleanup; 604 605 if (GNUNET_OK != 606 GNUNET_CONFIGURATION_get_value_string (cfg, 607 "authorization-sms", 608 "COMMAND", 609 &ctx->auth_command)) 610 { 611 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 612 "authorization-sms", 613 "COMMAND"); 614 regfree (&ctx->regex); 615 json_decref (ctx->messages); 616 GNUNET_free (ctx); 617 GNUNET_free (plugin); 618 return NULL; 619 } 620 return plugin; 621 } 622 623 624 /** 625 * Unload authorization plugin 626 * 627 * @param cls a `struct ANASTASIS_AuthorizationPlugin` 628 * @return NULL (always) 629 */ 630 void * 631 libanastasis_plugin_authorization_sms_done (void *cls); 632 633 /* declaration to fix compiler warning */ 634 void * 635 libanastasis_plugin_authorization_sms_done (void *cls) 636 { 637 struct ANASTASIS_AuthorizationPlugin *plugin = cls; 638 struct SMS_Context *ctx = plugin->cls; 639 640 GNUNET_free (ctx->auth_command); 641 regfree (&ctx->regex); 642 json_decref (ctx->messages); 643 GNUNET_free (ctx); 644 GNUNET_free (plugin); 645 return NULL; 646 }