/* 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 "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 { /** * 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; /** * Handle to interact with a authorization backend. */ const struct ANASTASIS_AuthorizationContext *ac; /** * 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->ac->db->event_listen (ctx->ac->db->cls, &espec.header, &bank_event_cb, as); } qs = ctx->ac->db->test_challenge_code_satisfied (ctx->ac->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->ac->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 `struct ANASTASIS_AuthorizationContext` * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` */ void * libanastasis_plugin_authorization_iban_init (void *cls) { struct ANASTASIS_AuthorizationContext *ac = cls; struct ANASTASIS_AuthorizationPlugin *plugin; const struct GNUNET_CONFIGURATION_Handle *cfg = ac->cfg; 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->ac = ac; plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); plugin->payment_plugin_managed = true; 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; json_decref (ctx->messages); GNUNET_free (ctx->business_iban); GNUNET_free (ctx->business_name); GNUNET_free (ctx); GNUNET_free (plugin); return NULL; }