anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

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 }