From 6f0432ed839d1f5a7a1e880f492ce162073b680b Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Sat, 14 Aug 2021 16:03:02 +0200 Subject: -first rough sketch for IBAN authorization plugin" --- .../anastasis_authorization_plugin_iban.c | 589 +++++++++++++++++++++ 1 file changed, 589 insertions(+) create mode 100644 src/authorization/anastasis_authorization_plugin_iban.c (limited to 'src/authorization/anastasis_authorization_plugin_iban.c') diff --git a/src/authorization/anastasis_authorization_plugin_iban.c b/src/authorization/anastasis_authorization_plugin_iban.c new file mode 100644 index 0000000..61e7f59 --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_iban.c @@ -0,0 +1,589 @@ +/* + This file is part of Anastasis + Copyright (C) 2021 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file anastasis_authorization_plugin_iban.c + * @brief authorization plugin wire transfer based + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include +#include +#include +#include +#include "anastasis_authorization_lib.h" +#include "anastasis_database_lib.h" +#include "anastasis_util_lib.h" +#include "iban.h" + +/** + * Saves the State of a authorization plugin. + */ +struct IBAN_Context +{ + + /** + * Authentication data to access our bank account. + */ + struct TALER_BANK_AuthenticationData auth; + + /** + * Messages of the plugin, read from a resource file. + */ + json_t *messages; + + /** + * IBAN of our business, must be credited in the SEPA + * wire transfer. + */ + char *business_iban; + + /** + * Name of our business, for the SEPA wire transfer. + */ + char *business_name; + + /** + * Database handle. + */ + struct ANASTASIS_DatabasePlugin *db; + + /** + * Amount we expect to be transferred. + */ + struct TALER_Amount expected_amount; + +}; + + +/** + * Saves the State of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * Public key of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Code which is sent to the user (here sent via IBAN) + */ + uint64_t code; + + /** + * Our plugin context. + */ + struct IBAN_Context *ctx; + + /** + * Function to call when we made progress. + */ + GNUNET_SCHEDULER_TaskCallback trigger; + + /** + * Closure for @e trigger. + */ + void *trigger_cls; + + /** + * holds the truth information + */ + char *iban_number; + + /** + * Our client connection, set if suspended. + */ + struct MHD_Connection *connection; + + /** + * Handler for database event we are waiting for. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Amount that was transferred. + */ + struct TALER_Amount amount; +}; + + +/** + * Obtain internationalized message @a msg_id from @a ctx using + * language preferences of @a conn. + * + * @param messages JSON object to lookup message from + * @param conn connection to lookup message for + * @param msg_id unique message ID + * @return NULL if message was not found + */ +static const char * +get_message (const json_t *messages, + struct MHD_Connection *conn, + const char *msg_id) +{ + const char *accept_lang; + + accept_lang = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == accept_lang) + accept_lang = "en_US"; + { + const char *ret; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_i18n_string (msg_id, + accept_lang, + &ret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (messages, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + return ret; + } +} + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed iban number for sending an IBAN, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the iban number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure with a `struct IBAN_Context` + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid iban number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +iban_validate (void *cls, + struct MHD_Connection *connection, + const char *truth_mime, + const char *data, + size_t data_length) +{ + char *iban_number; + char *emsg; + + iban_number = GNUNET_strndup (data, + data_length); + emsg = TALER_iban_validate (iban_number); + GNUNET_free (iban_number); + if (NULL != emsg) + { + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_IBAN_INVALID, + emsg)) + { + GNUNET_free (emsg); + return GNUNET_SYSERR; + } + GNUNET_free (emsg); + return GNUNET_NO; + } + return GNUNET_OK; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * Sends IBAN. + * + * @param cls closure with a `struct IBAN_Context` + * @param trigger function to call when we made progress + * @param trigger_cls closure for @a trigger + * @param truth_uuid Identifier of the challenge, to be (if possible) included in the + * interaction with the user + * @param code secret code that the user has to provide back to satisfy the challenge in + * the main anastasis protocol + * @param data input to validate (i.e. is it a valid iban number, etc.) + * @param data_length number of bytes in @a data + * @return state to track progress on the authorization operation, NULL on failure + */ +static struct ANASTASIS_AUTHORIZATION_State * +iban_start (void *cls, + GNUNET_SCHEDULER_TaskCallback trigger, + void *trigger_cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code, + const void *data, + size_t data_length) +{ + struct IBAN_Context *ctx = cls; + struct ANASTASIS_AUTHORIZATION_State *as; + + as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); + as->trigger = trigger; + as->trigger_cls = trigger_cls; + as->ctx = ctx; + as->truth_uuid = *truth_uuid; + as->code = code; + as->iban_number = GNUNET_strndup (data, + data_length); + return as; +} + + +/** + * Function called when we received a wire transfer + * with the respective code from the specified IBAN. + * + * @param cls our `struct ANASTASIS_AUHTORIZATION_State` + * @param extra string describing amount transferred + * @param extra_size number of byes in @a extra + */ +static void +bank_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + struct ANASTASIS_AUTHORIZATION_State *as = cls; + char *amount_s; + + amount_s = GNUNET_strndup (extra, + extra_size); + if (GNUNET_OK != + TALER_string_to_amount (amount_s, + &as->amount)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Expected amount in event notification, got `%s'\n", + amount_s); + } + GNUNET_free (amount_s); + MHD_resume_connection (as->connection); + as->trigger (as->trigger_cls); +} + + +/** + * Respond with instructions to the user how to + * satisfy the challenge. + * + * @param as our state + * @param connection connection to respond on + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +respond_with_challenge (struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection) +{ + struct IBAN_Context *ctx = as->ctx; + const char *mime; + const char *lang; + MHD_RESULT mres; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + + /* Build HTTP response */ + { + struct MHD_Response *resp; + const char *end; + size_t slen; + + slen = strlen (as->iban_number); + if (slen > 4) + end = &as->iban_number[slen - 4]; + else + end = &as->iban_number[slen / 2]; + + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_string ("method", + "iban"), + TALER_JSON_pack_amount ("challenge_amount", + &ctx->expected_amount), + GNUNET_JSON_pack_string ("credit_iban", + ctx->business_iban), + GNUNET_JSON_pack_string ("business_name", + ctx->business_name), + GNUNET_JSON_pack_uint64 ("wire_transfer_subject", + as->code), + GNUNET_JSON_pack_string ("debit_account_hint", + end)); + } + else + { + size_t reply_len; + char *reply; + + reply_len = GNUNET_asprintf (&reply, + get_message (ctx->messages, + connection, + "instructions"), + TALER_amount2s (&ctx->expected_amount), + ctx->business_name, + ctx->business_iban, + (unsigned long long) as->code, + end); + resp = MHD_create_response_from_buffer (reply_len, + reply, + MHD_RESPMEM_MUST_COPY); + GNUNET_free (reply); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + } + mres = MHD_queue_response (connection, + MHD_HTTP_ACCEPTED, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; + } + +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send IBAN or e-mail or launch video identification. + * + * @param as authorization state + * @param timeout how long do we have to produce a reply + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +iban_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct GNUNET_TIME_Absolute timeout, + struct MHD_Connection *connection) +{ + struct IBAN_Context *ctx = as->ctx; + MHD_RESULT mres; + enum GNUNET_DB_QueryStatus qs; + struct MHD_Response *resp; + + if (NULL == as->eh) + { + struct IbanEventP espec = { + .header.size = htons (sizeof (espec)), + .header.type = htons (TALER_DBEVENT_ANASTASIS_AUTH_IBAN_TRANSFER), + .code = GNUNET_htonll (as->code) + }; + + GNUNET_CRYPTO_hash (as->iban_number, + strlen (as->iban_number), + &espec.debit_iban_hash); + as->eh = ctx->db->event_listen (ctx->db->cls, + &espec.header, + &bank_event_cb, + as); + } + qs = ctx->db->test_challenge_code_satisfied (ctx->db->cls, + &as->truth_uuid, + as->code); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, + "test_challenge_code_satisfied"); + mres = MHD_queue_response (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + if (GNUNET_TIME_absolute_is_future (timeout)) + { + as->connection = connection; + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + return respond_with_challenge (as, + connection); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return ANASTASIS_AUTHORIZATION_RES_FINISHED; + } + /* should be impossible */ + GNUNET_break (0); + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +iban_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + struct IBAN_Context *ctx = as->ctx; + + if (NULL != as->eh) + { + ctx->db->event_listen_cancel (as->eh); + as->eh = NULL; + } + GNUNET_free (as->iban_number); + GNUNET_free (as); +} + + +/** + * Initialize email based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_iban_init (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin; + struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct IBAN_Context *ctx; + + ctx = GNUNET_new (struct IBAN_Context); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "authorization-iban", + "CREDIT_IBAN", + &ctx->business_iban)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-iban", + "CREDIT_IBAN"); + GNUNET_free (ctx); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "authorization-iban", + "BUSINESS_NAME", + &ctx->business_name)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-iban", + "BUSINESS_NAME"); + GNUNET_free (ctx->business_iban); + GNUNET_free (ctx); + return NULL; + } + { + char *fn; + json_error_t err; + + GNUNET_asprintf (&fn, + "%sauthorization-iban-messages.json", + GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR)); + ctx->messages = json_load_file (fn, + JSON_REJECT_DUPLICATES, + &err); + if (NULL == ctx->messages) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load messages from `%s': %s at %d:%d\n", + fn, + err.text, + err.line, + err.column); + GNUNET_free (ctx->business_iban); + GNUNET_free (ctx->business_name); + GNUNET_free (fn); + GNUNET_free (ctx); + return NULL; + } + GNUNET_free (fn); + } + ctx->db = ANASTASIS_DB_plugin_load (cfg); + if (NULL == ctx->db) + { + json_decref (ctx->messages); + GNUNET_free (ctx->business_iban); + GNUNET_free (ctx->business_name); + GNUNET_free (ctx); + return NULL; + } + if (GNUNET_OK != + TALER_BANK_auth_parse_cfg (cfg, + "authorization-iban", + &ctx->auth)) + { + ANASTASIS_DB_plugin_unload (ctx->db); + json_decref (ctx->messages); + GNUNET_free (ctx->business_iban); + GNUNET_free (ctx->business_name); + GNUNET_free (ctx); + return NULL; + } + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->code_validity_period = GNUNET_TIME_UNIT_MONTHS; + plugin->code_rotation_period = GNUNET_TIME_UNIT_WEEKS; + plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_FOREVER_REL; + plugin->cls = ctx; + plugin->validate = &iban_validate; + plugin->start = &iban_start; + plugin->process = &iban_process; + plugin->cleanup = &iban_cleanup; + + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_iban_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + struct IBAN_Context *ctx = plugin->cls; + + ANASTASIS_DB_plugin_unload (ctx->db); + TALER_BANK_auth_free (&ctx->auth); + json_decref (ctx->messages); + GNUNET_free (ctx->business_iban); + GNUNET_free (ctx->business_name); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; +} -- cgit v1.2.3