anastasis

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

anastasis_authorization_plugin_email.c (19525B)


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