/* This totp 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 totp COPYING.GPL. If not, see */ /** * @totp anastasis_authorization_plugin_totp.c * @brief authorization plugin using totp * @author Christian Grothoff */ #include "platform.h" #include "anastasis_authorization_plugin.h" #include #include #include "anastasis_database_lib.h" #include /** * How many retries do we allow per code? */ #define INITIAL_RETRY_COUNTER 3 /** * How long is a TOTP code valid? */ #define TOTP_VALIDITY_PERIOD GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_SECONDS, 30) /** * Range of time we allow (plus-minus). */ #define TIME_INTERVAL_RANGE 2 /** * How long is the shared secret in bytes? */ #define SECRET_LEN 32 /** * Saves the state of a authorization process */ struct ANASTASIS_AUTHORIZATION_State { /** * UUID of the challenge which is authorised */ struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; /** * Was the challenge satisfied? */ struct GNUNET_HashCode valid_replies[TIME_INTERVAL_RANGE * 2 + 1]; /** * Our context. */ const struct ANASTASIS_AuthorizationContext *ac; }; /** * Validate @a data is a well-formed input into the challenge method, * i.e. @a data is a well-formed phone number for sending an SMS, or * a well-formed e-mail address for sending an e-mail. Not expected to * check that the phone 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 `const struct ANASTASIS_AuthorizationContext *` * @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 phone 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 totp_validate (void *cls, struct MHD_Connection *connection, const char *truth_mime, const char *data, size_t data_length) { (void) cls; (void) truth_mime; (void) connection; if (NULL == data) { GNUNET_break_op (0); if (MHD_NO == TALER_MHD_reply_with_error (connection, MHD_HTTP_EXPECTATION_FAILED, TALER_EC_ANASTASIS_TOTP_KEY_MISSING, NULL)) return GNUNET_SYSERR; return GNUNET_NO; } if (SECRET_LEN != data_length) { GNUNET_break_op (0); if (MHD_NO == TALER_MHD_reply_with_error (connection, MHD_HTTP_EXPECTATION_FAILED, TALER_EC_ANASTASIS_TOTP_KEY_INVALID, NULL)) return GNUNET_SYSERR; return GNUNET_NO; } return GNUNET_OK; } /** * Compute TOTP code at current time with offset * @a time_off for the @a key. * * @param time_off offset to apply when computing the code * @param key input key material * @param key_size number of bytes in @a key * @return TOTP code at this time */ static uint64_t compute_totp (int time_off, const void *key, size_t key_size) { struct GNUNET_TIME_Absolute now; time_t t; uint64_t ctr; uint8_t hmac[16]; /* SHA1: 16 bytes */ now = GNUNET_TIME_absolute_get (); while (time_off < 0) { now = GNUNET_TIME_absolute_subtract (now, TOTP_VALIDITY_PERIOD); time_off++; } while (time_off > 0) { now = GNUNET_TIME_absolute_add (now, TOTP_VALIDITY_PERIOD); time_off--; } t = now.abs_value_us / GNUNET_TIME_UNIT_SECONDS.rel_value_us; ctr = GNUNET_htonll (t / 30LLU); { gcry_md_hd_t md; const unsigned char *mc; GNUNET_assert (GPG_ERR_NO_ERROR == gcry_md_open (&md, GCRY_MD_SHA1, GCRY_MD_FLAG_HMAC)); gcry_md_setkey (md, key, key_size); gcry_md_write (md, &ctr, sizeof (ctr)); mc = gcry_md_read (md, GCRY_MD_SHA1); GNUNET_assert (NULL != mc); memcpy (hmac, mc, sizeof (hmac)); gcry_md_close (md); } { uint32_t code = 0; for (int count = 0; count < 4; count++) code += hmac[(hmac[sizeof (hmac) - 1] & 0x0f) + 3 - count] << 8 * count; code &= 0x7fffffff; #if VAR_DIGITS if (digits == 6) code = code % 1000000; else if (digits == 7) code = code % 10000000; else if (digits == 8) code = code % 100000000; #else code = code % 1000000; #endif return code; } } /** * Begin issuing authentication challenge to user based on @a data. * * @param cls closure * @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 always 0 (direct validation, backend does * not generate a code in this mode) * @param data truth for input to validate (i.e. the shared secret) * @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 * totp_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) { const struct ANASTASIS_AuthorizationContext *ac = cls; struct ANASTASIS_AUTHORIZATION_State *as; uint64_t want; unsigned int off = 0; GNUNET_assert (0 == code); as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); as->ac = ac; as->truth_uuid = *truth_uuid; for (int i = -TIME_INTERVAL_RANGE; i < TIME_INTERVAL_RANGE; i++) { want = compute_totp (i, data, data_length); ANASTASIS_hash_answer (want, &as->valid_replies[off++]); } return as; } /** * Begin issuing authentication challenge to user based on @a data. * * @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 totp_process (struct ANASTASIS_AUTHORIZATION_State *as, struct GNUNET_TIME_Absolute timeout, struct MHD_Connection *connection) { MHD_RESULT mres; const char *mime; const char *lang; const char *challenge_response_s; struct GNUNET_HashCode challenge_response; challenge_response_s = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "response"); if ( (NULL == challenge_response_s) || (GNUNET_OK != GNUNET_CRYPTO_hash_from_string (challenge_response_s, &challenge_response)) ) { GNUNET_break_op (0); mres = TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "response"); if (MHD_YES != mres) return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; return ANASTASIS_AUTHORIZATION_RES_FAILED; } for (unsigned int i = 0; i<=TIME_INTERVAL_RANGE * 2; i++) if (0 == GNUNET_memcmp (&challenge_response, &as->valid_replies[i])) return ANASTASIS_AUTHORIZATION_RES_FINISHED; 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; struct GNUNET_TIME_Absolute now; now = GNUNET_TIME_absolute_get (); if (TALER_MHD_xmime_matches (mime, "application/json")) { resp = TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_time_abs ("server_time", now)); } else { size_t response_size; char *response; // FIXME: i18n of the message based on 'lang' ... response_size = GNUNET_asprintf (&response, "Server time: %s", GNUNET_STRINGS_absolute_time_to_string (now)); resp = MHD_create_response_from_buffer (response_size, response, MHD_RESPMEM_MUST_COPY); 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_FORBIDDEN, resp); MHD_destroy_response (resp); } if (MHD_YES != mres) return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; return ANASTASIS_AUTHORIZATION_RES_SUCCESS; } /** * Free internal state associated with @a as. * * @param as state to clean up */ static void totp_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) { GNUNET_free (as); } /** * Initialize Totp based authorization plugin * * @param cls a configuration instance * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` */ void * libanastasis_plugin_authorization_totp_init (void *cls) { const struct ANASTASIS_AuthorizationContext *ac = cls; struct ANASTASIS_AuthorizationPlugin *plugin; plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); plugin->cls = (void *) ac; plugin->user_provided_code = true; plugin->retry_counter = INITIAL_RETRY_COUNTER; plugin->code_validity_period = TOTP_VALIDITY_PERIOD; plugin->code_rotation_period = plugin->code_validity_period; plugin->code_retransmission_frequency = plugin->code_validity_period; plugin->validate = &totp_validate; plugin->start = &totp_start; plugin->process = &totp_process; plugin->cleanup = &totp_cleanup; return plugin; } /** * Unload authorization plugin * * @param cls a `struct ANASTASIS_AuthorizationPlugin` * @return NULL (always) */ void * libanastasis_plugin_authorization_totp_done (void *cls) { struct ANASTASIS_AuthorizationPlugin *plugin = cls; GNUNET_free (plugin); return NULL; }