/* This file is part of Anastasis Copyright (C) 2020, 2021 Anastasis SARL Anastasis is free software; you can redistribute it and/or modify it under the terms of the GNU 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 General Public License for more details. You should have received a copy of the GNU General Public License along with Anastasis; see the file COPYING.GPL. If not, see */ /** * @file reducer/anastasis_api_recovery_redux.c * @brief anastasis reducer recovery api * @author Christian Grothoff * @author Dominik Meister * @author Dennis Neufeld */ #include #include #include "anastasis_redux.h" #include "anastasis_error_codes.h" #include "anastasis_api_redux.h" #define GENERATE_STRING(STRING) #STRING, static const char *recovery_strings[] = { ANASTASIS_RECOVERY_STATES (GENERATE_STRING) }; #undef GENERATE_STRING enum ANASTASIS_RecoveryState ANASTASIS_recovery_state_from_string_ (const char *state_string) { for (enum ANASTASIS_RecoveryState i = 0; i < sizeof (recovery_strings) / sizeof(*recovery_strings); i++) if (0 == strcmp (state_string, recovery_strings[i])) return i; return ANASTASIS_RECOVERY_STATE_INVALID; } const char * ANASTASIS_recovery_state_to_string_ (enum ANASTASIS_RecoveryState rs) { if ( (rs < 0) || (rs >= sizeof (recovery_strings) / sizeof(*recovery_strings)) ) { GNUNET_break_op (0); return NULL; } return recovery_strings[rs]; } static void set_state (json_t *state, enum ANASTASIS_RecoveryState new_recovery_state) { GNUNET_assert ( 0 == json_object_set_new ( state, "recovery_state", json_string (ANASTASIS_recovery_state_to_string_ (new_recovery_state)))); } /** * Returns an initial ANASTASIS recovery state. * * @return NULL on failure */ json_t * ANASTASIS_recovery_start (const struct GNUNET_CONFIGURATION_Handle *cfg) { json_t *initial_state; const char *external_reducer = ANASTASIS_REDUX_probe_external_reducer (); if (NULL != external_reducer) { int pipefd_stdout[2]; pid_t pid = 0; int status; FILE *reducer_stdout; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Using external reducer '%s' for recovery start status\n", external_reducer); GNUNET_assert (0 == pipe (pipefd_stdout)); pid = fork (); if (pid == 0) { (void) close (pipefd_stdout[0]); (void) dup2 (pipefd_stdout[1], STDOUT_FILENO); execlp (external_reducer, external_reducer, "-r", NULL); GNUNET_assert (0); } close (pipefd_stdout[1]); reducer_stdout = fdopen (pipefd_stdout[0], "r"); { json_error_t err; initial_state = json_loadf (reducer_stdout, 0, &err); if (NULL == initial_state) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "External reducer did not output valid JSON: %s:%d:%d %s\n", err.source, err.line, err.column, err.text); GNUNET_assert (0 == fclose (reducer_stdout)); waitpid (pid, &status, 0); return NULL; } } GNUNET_assert (NULL != initial_state); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Waiting for external reducer to terminate.\n"); GNUNET_assert (0 == fclose (reducer_stdout)); reducer_stdout = NULL; waitpid (pid, &status, 0); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "External reducer finished with exit status '%d'\n", status); return initial_state; } (void) cfg; initial_state = ANASTASIS_REDUX_load_continents_ (); if (NULL == initial_state) return NULL; GNUNET_assert ( 0 == json_object_set_new (initial_state, "reducer_type", json_string ("recovery"))); set_state (initial_state, ANASTASIS_RECOVERY_STATE_CONTINENT_SELECTING); return initial_state; } /** * Context for a "select_challenge" operation. */ struct SelectChallengeContext { /** * Handle we returned for cancellation of the operation. */ struct ANASTASIS_ReduxAction ra; /** * UUID of the challenge selected by the user for solving. */ struct ANASTASIS_CRYPTO_TruthUUIDP uuid; /** * Which timeout was set for the operation? */ struct GNUNET_TIME_Relative timeout; /** * Overall recovery action. */ struct ANASTASIS_Recovery *r; /** * Function to call with the next state. */ ANASTASIS_ActionCallback cb; /** * Closure for @e cb. */ void *cb_cls; /** * Our state. */ json_t *state; /** * Our arguments (like answers to the challenge, if already provided). */ json_t *args; /** * Task scheduled for delayed success reporting. Needed to make * sure that the solved challenge was really the final result, * cancelled if the solved challenge resulted in the secret being * recovered. */ struct GNUNET_SCHEDULER_Task *delayed_report; /** * Payment secret, if we are in the "pay" state. */ struct ANASTASIS_PaymentSecretP ps; /** * Application asked us to only poll for existing * asynchronous challenges, and not to being a * new one. */ bool poll_only; }; /** * Cleanup a select challenge context. * * @param cls a `struct SelectChallengeContext *` */ static void sctx_free (void *cls) { struct SelectChallengeContext *sctx = cls; if (NULL != sctx->r) { ANASTASIS_recovery_abort (sctx->r); sctx->r = NULL; } json_decref (sctx->state); json_decref (sctx->args); if (NULL != sctx->delayed_report) { GNUNET_SCHEDULER_cancel (sctx->delayed_report); sctx->delayed_report = NULL; } GNUNET_free (sctx); } /** * Call the action callback with an error result * * @param cb action callback to call * @param cb_cls closure for @a cb * @param rc error code to translate to JSON */ void fail_by_error (ANASTASIS_ActionCallback cb, void *cb_cls, enum ANASTASIS_RecoveryStatus rc) { const char *msg = NULL; enum TALER_ErrorCode ec = TALER_EC_INVALID; switch (rc) { case ANASTASIS_RS_SUCCESS: GNUNET_assert (0); break; case ANASTASIS_RS_POLICY_DOWNLOAD_FAILED: msg = gettext_noop ("download failed due to unexpected network issue"); ec = TALER_EC_ANASTASIS_REDUCER_NETWORK_FAILED; break; case ANASTASIS_RS_POLICY_DOWNLOAD_NO_POLICY: GNUNET_break (0); msg = gettext_noop ("policy document returned was malformed"); ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; break; case ANASTASIS_RS_POLICY_DOWNLOAD_TOO_BIG: GNUNET_break (0); msg = gettext_noop ("policy document too large for client memory"); ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; break; case ANASTASIS_RS_POLICY_DOWNLOAD_INVALID_COMPRESSION: GNUNET_break (0); msg = gettext_noop ("failed to decompress policy document"); ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; break; case ANASTASIS_RS_POLICY_DOWNLOAD_NO_JSON: GNUNET_break (0); msg = gettext_noop ("policy document returned was not in JSON format"); ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; break; case ANASTASIS_RS_POLICY_MALFORMED_JSON: GNUNET_break (0); msg = gettext_noop ( "policy document returned was not in required JSON format"); ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; break; case ANASTASIS_RS_POLICY_SERVER_ERROR: msg = gettext_noop ("Anastasis server reported transient internal error"); ec = TALER_EC_ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED; break; case ANASTASIS_RS_POLICY_GONE: msg = gettext_noop ("policy document no longer exists"); ec = TALER_EC_ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED; break; case ANASTASIS_RS_POLICY_UNKNOWN: msg = gettext_noop ("account unknown to Anastasis server"); ec = TALER_EC_ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED; break; } ANASTASIS_redux_fail_ (cb, cb_cls, ec, msg); } /** * This function is called whenever the recovery process ends. * On success, the secret is returned in @a secret. * * @param cls handle for the callback * @param rc error code * @param secret contains the core secret which is passed to the user * @param secret_size defines the size of the core secret */ static void core_secret_cb (void *cls, enum ANASTASIS_RecoveryStatus rc, const void *secret, size_t secret_size) { struct SelectChallengeContext *sctx = cls; sctx->r = NULL; if (ANASTASIS_RS_SUCCESS == rc) { json_t *jsecret; jsecret = json_loadb (secret, secret_size, JSON_REJECT_DUPLICATES, NULL); if (NULL == jsecret) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_SECRET_MALFORMED, NULL); sctx_free (sctx); return; } GNUNET_assert (0 == json_object_set_new (sctx->state, "core_secret", jsecret)); set_state (sctx->state, ANASTASIS_RECOVERY_STATE_RECOVERY_FINISHED); sctx->cb (sctx->cb_cls, TALER_EC_NONE, sctx->state); sctx_free (sctx); return; } fail_by_error (sctx->cb, sctx->cb_cls, rc); sctx_free (sctx); } /** * A challenge was solved, but we are not yet finished. * Report to caller that the challenge was completed. * * @param cls a `struct SelectChallengeContext` */ static void report_solved (void *cls) { struct SelectChallengeContext *sctx = cls; sctx->delayed_report = NULL; set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); sctx->cb (sctx->cb_cls, TALER_EC_NONE, sctx->state); sctx_free (sctx); } /** * Find challenge of @a uuid in @a state under "recovery_information". * * @param state the state to search * @param uuid the UUID to search for * @return NULL on error, otherwise challenge entry; RC is NOT incremented */ static json_t * find_challenge_in_ri (json_t *state, const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid) { struct ANASTASIS_CRYPTO_TruthUUIDP u; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("uuid", &u), GNUNET_JSON_spec_end () }; json_t *ri; json_t *challenges; json_t *challenge; size_t index; ri = json_object_get (state, "recovery_information"); if (NULL == ri) { GNUNET_break (0); return NULL; } challenges = json_object_get (ri, "challenges"); if (NULL == challenges) { GNUNET_break (0); return NULL; } json_array_foreach (challenges, index, challenge) { if (GNUNET_OK != GNUNET_JSON_parse (challenge, spec, NULL, NULL)) { GNUNET_break (0); return NULL; } if (0 == GNUNET_memcmp (&u, uuid)) { return challenge; } } return NULL; } /** * Find challenge of @a uuid in @a state under "challenges". * * @param state the state to search * @param uuid the UUID to search for * @return NULL on error, otherwise challenge entry; RC is NOT incremented */ static json_t * find_challenge_in_cs (json_t *state, const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid) { json_t *rd = json_object_get (state, "recovery_document"); json_t *cs = json_object_get (rd, "challenges"); json_t *c; size_t off; json_array_foreach (cs, off, c) { struct ANASTASIS_CRYPTO_TruthUUIDP u; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("uuid", &u), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (c, spec, NULL, NULL)) { GNUNET_break (0); continue; } if (0 != GNUNET_memcmp (uuid, &u)) continue; return c; } return NULL; } /** * Defines a callback for the response status for a challenge start * operation. * * @param cls a `struct SelectChallengeContext *` * @param csr response details */ static void start_feedback_cb ( void *cls, const struct ANASTASIS_ChallengeStartResponse *csr) { struct SelectChallengeContext *sctx = cls; const struct ANASTASIS_ChallengeDetails *cd; char uuid[sizeof (cd->uuid) * 2]; char *end; json_t *feedback; cd = ANASTASIS_challenge_get_details (csr->challenge); end = GNUNET_STRINGS_data_to_string (&cd->uuid, sizeof (cd->uuid), uuid, sizeof (uuid)); GNUNET_assert (NULL != end); *end = '\0'; feedback = json_object_get (sctx->state, "challenge_feedback"); if (NULL == feedback) { feedback = json_object (); GNUNET_assert (0 == json_object_set_new (sctx->state, "challenge_feedback", feedback)); } switch (csr->cs) { case ANASTASIS_CHALLENGE_START_STATUS_FILENAME_PROVIDED: { json_t *instructions; char *hint; GNUNET_asprintf (&hint, _ ("Required TAN can be found in `%s'"), csr->details.tan_filename); instructions = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "code-in-file"), GNUNET_JSON_pack_string ("filename", csr->details.tan_filename), GNUNET_JSON_pack_string ("display_hint", hint)); GNUNET_free (hint); GNUNET_assert (0 == json_object_set_new (feedback, uuid, instructions)); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); sctx->cb (sctx->cb_cls, TALER_EC_NONE, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_START_STATUS_TAN_SENT_HINT_PROVIDED: { json_t *instructions; char *hint; GNUNET_asprintf (&hint, _ ("TAN code was sent to `%s'"), csr->details.tan_address_hint); instructions = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "send-to-address"), GNUNET_JSON_pack_string ("address_hint", csr->details.tan_address_hint), GNUNET_JSON_pack_string ("display_hint", hint)); GNUNET_free (hint); GNUNET_assert (0 == json_object_set_new (feedback, uuid, instructions)); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); sctx->cb (sctx->cb_cls, TALER_EC_NONE, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_START_STATUS_TAN_ALREADY_SENT: { json_t *instructions; char *hint; GNUNET_asprintf (&hint, _ ("TAN code already sent.")); instructions = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "send-to-address"), GNUNET_JSON_pack_string ("display_hint", hint)); GNUNET_free (hint); GNUNET_assert (0 == json_object_set_new (feedback, uuid, instructions)); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); sctx->cb (sctx->cb_cls, TALER_EC_NONE, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_START_STATUS_PAYMENT_REQUIRED: { json_t *pay; char *hint; GNUNET_asprintf (&hint, _ ("Taler payment to `%s' required"), csr->details.payment_required.taler_pay_uri); pay = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "taler-payment"), GNUNET_JSON_pack_string ( "taler_pay_uri", csr->details.payment_required.taler_pay_uri), GNUNET_JSON_pack_string ("provider", cd->provider_url), GNUNET_JSON_pack_string ("display_hint", hint), GNUNET_JSON_pack_data_auto ( "payment_secret", &csr->details.payment_required.payment_secret)); GNUNET_free (hint); GNUNET_assert (0 == json_object_set_new (feedback, uuid, pay)); } /* Remember payment secret for later (once application claims it paid) */ { json_t *challenge = find_challenge_in_ri (sctx->state, &cd->uuid); GNUNET_assert (NULL != challenge); GNUNET_assert (0 == json_object_set_new ( challenge, "payment_secret", GNUNET_JSON_from_data_auto ( &csr->details.payment_required.payment_secret))); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING); sctx->cb (sctx->cb_cls, TALER_EC_NONE, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_START_STATUS_SERVER_FAILURE: { json_t *err; err = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "server-failure"), GNUNET_JSON_pack_uint64 ("http_status", csr->http_status), GNUNET_JSON_pack_uint64 ("error_code", csr->ec)); GNUNET_assert (0 == json_object_set_new (feedback, uuid, err)); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); sctx->cb (sctx->cb_cls, csr->ec, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_START_STATUS_TRUTH_UNKNOWN: { json_t *err; err = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "truth-unknown"), GNUNET_JSON_pack_uint64 ("http_status", csr->http_status), GNUNET_JSON_pack_uint64 ("error_code", TALER_EC_ANASTASIS_TRUTH_UNKNOWN)); GNUNET_assert (0 == json_object_set_new (feedback, uuid, err)); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); sctx->cb (sctx->cb_cls, TALER_EC_ANASTASIS_TRUTH_UNKNOWN, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_START_STATUS_BANK_TRANSFER_REQUIRED: { json_t *reply; json_t *c; char *hint; c = find_challenge_in_cs (sctx->state, &cd->uuid); if (NULL == c) { GNUNET_break (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, NULL); sctx_free (sctx); return; } GNUNET_assert (0 == json_object_set_new (c, "async", json_true ())); GNUNET_assert ( 0 == json_object_set_new ( c, "answer-pin", json_integer ( csr->details.bank_transfer_required.answer_code))); GNUNET_asprintf (&hint, _ ("Wire %s to %s (%s) with subject %s\n"), TALER_amount2s ( &csr->details.bank_transfer_required.amount), csr->details.bank_transfer_required.target_iban, csr->details.bank_transfer_required.target_business_name, csr->details.bank_transfer_required.wire_transfer_subject); reply = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "iban-instructions"), GNUNET_JSON_pack_string ( "target_iban", csr->details.bank_transfer_required.target_iban), GNUNET_JSON_pack_string ( "display_hint", hint), GNUNET_JSON_pack_string ( "target_business_name", csr->details.bank_transfer_required.target_business_name), GNUNET_JSON_pack_string ( "wire_transfer_subject", csr->details.bank_transfer_required.wire_transfer_subject), TALER_JSON_pack_amount ( "challenge_amount", &csr->details.bank_transfer_required.amount)); GNUNET_free (hint); GNUNET_assert (0 == json_object_set_new (feedback, uuid, reply)); } GNUNET_assert (0 == json_object_set_new (sctx->state, "selected_challenge_uuid", GNUNET_JSON_from_data_auto ( &cd->uuid))); set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); sctx->cb (sctx->cb_cls, TALER_EC_NONE, sctx->state); sctx_free (sctx); return; } GNUNET_break (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, NULL); sctx_free (sctx); } /** * Defines a callback for the response status for a challenge answer * operation. * * @param cls a `struct SelectChallengeContext *` * @param csr response details */ static void answer_feedback_cb ( void *cls, const struct ANASTASIS_ChallengeAnswerResponse *csr) { struct SelectChallengeContext *sctx = cls; const struct ANASTASIS_ChallengeDetails *cd; char uuid[sizeof (cd->uuid) * 2]; char *end; json_t *feedback; cd = ANASTASIS_challenge_get_details (csr->challenge); end = GNUNET_STRINGS_data_to_string (&cd->uuid, sizeof (cd->uuid), uuid, sizeof (uuid)); GNUNET_assert (NULL != end); *end = '\0'; feedback = json_object_get (sctx->state, "challenge_feedback"); if (NULL == feedback) { feedback = json_object (); GNUNET_assert (0 == json_object_set_new (sctx->state, "challenge_feedback", feedback)); } switch (csr->cs) { case ANASTASIS_CHALLENGE_ANSWER_STATUS_SOLVED: { json_t *rd; rd = ANASTASIS_recovery_serialize (sctx->r); if (NULL == rd) { GNUNET_break (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "unable to serialize recovery state"); sctx_free (sctx); return; } GNUNET_assert (0 == json_object_set_new (sctx->state, "recovery_document", rd)); } { json_t *solved; solved = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "solved")); GNUNET_assert (0 == json_object_set_new (feedback, uuid, solved)); } /* Delay reporting challenge success, as we MAY still also see a secret recovery success (and we can only call the callback once) */ sctx->delayed_report = GNUNET_SCHEDULER_add_now (&report_solved, sctx); return; case ANASTASIS_CHALLENGE_ANSWER_STATUS_INVALID_ANSWER: { json_t *instructions; instructions = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "incorrect-answer"), GNUNET_JSON_pack_uint64 ("error_code", csr->ec)); GNUNET_assert (0 == json_object_set_new (feedback, uuid, instructions)); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); sctx->cb (sctx->cb_cls, TALER_EC_NONE, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_ANSWER_STATUS_PAYMENT_REQUIRED: { json_t *pay; char *hint; GNUNET_asprintf (&hint, _ ("Taler payment to `%s' required"), csr->details.payment_required.taler_pay_uri); pay = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "taler-payment"), GNUNET_JSON_pack_string ( "taler_pay_uri", csr->details.payment_required.taler_pay_uri), GNUNET_JSON_pack_string ( "display_hint", hint), GNUNET_JSON_pack_string ("provider", cd->provider_url), GNUNET_JSON_pack_data_auto ( "payment_secret", &csr->details.payment_required.payment_secret)); GNUNET_free (hint); GNUNET_assert (0 == json_object_set_new (feedback, uuid, pay)); } /* Remember payment secret for later (once application claims it paid) */ { json_t *challenge = find_challenge_in_ri (sctx->state, &cd->uuid); GNUNET_assert (NULL != challenge); GNUNET_assert (0 == json_object_set_new ( challenge, "payment_secret", GNUNET_JSON_from_data_auto ( &csr->details.payment_required.payment_secret))); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING); sctx->cb (sctx->cb_cls, TALER_EC_NONE, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_ANSWER_STATUS_SERVER_FAILURE: { json_t *err; err = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "server-failure"), GNUNET_JSON_pack_uint64 ("http_status", csr->http_status), GNUNET_JSON_pack_uint64 ("error_code", csr->ec)); GNUNET_assert (0 == json_object_set_new (feedback, uuid, err)); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); sctx->cb (sctx->cb_cls, csr->ec, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_ANSWER_STATUS_TRUTH_UNKNOWN: { json_t *err; err = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "truth-unknown"), GNUNET_JSON_pack_uint64 ("http_status", csr->http_status), GNUNET_JSON_pack_uint64 ("error_code", TALER_EC_ANASTASIS_TRUTH_UNKNOWN)); GNUNET_assert (0 == json_object_set_new (feedback, uuid, err)); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); sctx->cb (sctx->cb_cls, TALER_EC_ANASTASIS_TRUTH_UNKNOWN, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_ANSWER_STATUS_RATE_LIMIT_EXCEEDED: { json_t *err; char *hint; GNUNET_asprintf ( &hint, _ ("exceeded limit of %llu attempts in %s"), (unsigned long long) csr->details.rate_limit_exceeded.request_limit, GNUNET_TIME_relative2s ( csr->details.rate_limit_exceeded.request_frequency, true)); err = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ( "state", "rate-limit-exceeded"), GNUNET_JSON_pack_string ( "display_hint", hint), GNUNET_JSON_pack_uint64 ( "request_limit", csr->details.rate_limit_exceeded.request_limit), GNUNET_JSON_pack_time_rel ( "request_frequency", csr->details.rate_limit_exceeded.request_frequency), GNUNET_JSON_pack_uint64 ( "error_code", TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED)); GNUNET_free (hint); GNUNET_assert (0 == json_object_set_new (feedback, uuid, err)); } set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); sctx->cb (sctx->cb_cls, TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, sctx->state); sctx_free (sctx); return; } GNUNET_break (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, NULL); sctx_free (sctx); } /** * Callback which passes back the recovery document and its possible * policies. Also passes back the version of the document for the user * to check. * * We find the selected challenge and try to answer it (or begin * the process). * * @param cls a `struct SelectChallengeContext *` * @param ri recovery information struct which contains the policies */ static void solve_challenge_cb (void *cls, const struct ANASTASIS_RecoveryInformation *ri) { struct SelectChallengeContext *sctx = cls; const struct ANASTASIS_PaymentSecretP *psp = NULL; struct ANASTASIS_PaymentSecretP ps; struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; struct GNUNET_JSON_Specification tspec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_relative_time ("timeout", &timeout), NULL), GNUNET_JSON_spec_end () }; struct GNUNET_JSON_Specification pspec[] = { GNUNET_JSON_spec_fixed_auto ("payment_secret", &ps), GNUNET_JSON_spec_end () }; json_t *challenge; if (NULL == ri) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "recovery information could not be deserialized"); sctx_free (sctx); return; } if ( (NULL != sctx->args) && (GNUNET_OK != GNUNET_JSON_parse (sctx->args, tspec, NULL, NULL)) ) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'timeout' malformed"); sctx_free (sctx); return; } /* resume all async, unsolved challenges */ { bool poll_started = false; for (unsigned int i = 0; ics_len; i++) { struct ANASTASIS_Challenge *ci = ri->cs[i]; const struct ANASTASIS_ChallengeDetails *cd; json_t *challenge; json_t *pin; cd = ANASTASIS_challenge_get_details (ci); if (cd->solved || (! cd->async) ) continue; challenge = find_challenge_in_cs (sctx->state, &cd->uuid); if (NULL == challenge) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "challenge not found"); sctx_free (sctx); return; } pin = json_object_get (challenge, "answer-pin"); if (! json_is_integer (pin)) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "async challenge 'answer-pin' not found"); sctx_free (sctx); return; } if (GNUNET_OK != ANASTASIS_challenge_answer2 (ci, psp, timeout, json_integer_value (pin), &answer_feedback_cb, sctx)) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "Failed to begin answering asynchronous challenge"); sctx_free (sctx); return; } poll_started = true; } if (sctx->poll_only) { if (! poll_started) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID, "no challenge available for polling"); return; } /* only polling, do not start new challenges */ return; } } /* end resuming async challenges */ /* Check if we got a payment_secret */ challenge = find_challenge_in_ri (sctx->state, &sctx->uuid); if (NULL == challenge) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "challenge not found"); sctx_free (sctx); return; } if (NULL != json_object_get (sctx->args, "payment_secret")) { /* check if we got payment secret in args */ if (GNUNET_OK != GNUNET_JSON_parse (sctx->args, pspec, NULL, NULL)) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'payment_secret' malformed"); sctx_free (sctx); return; } psp = &ps; } else if (NULL != json_object_get (challenge, "payment_secret")) { if (GNUNET_OK != GNUNET_JSON_parse (challenge, pspec, NULL, NULL)) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'payment_secret' malformed"); sctx_free (sctx); return; } psp = &ps; } /* start or solve selected challenge */ for (unsigned int i = 0; ics_len; i++) { struct ANASTASIS_Challenge *ci = ri->cs[i]; const struct ANASTASIS_ChallengeDetails *cd; int ret; json_t *c; cd = ANASTASIS_challenge_get_details (ci); if (cd->async) continue; /* handled above */ if (0 != GNUNET_memcmp (&sctx->uuid, &cd->uuid)) continue; if (cd->solved) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "Selected challenge already solved"); sctx_free (sctx); return; } c = find_challenge_in_cs (sctx->state, &cd->uuid); GNUNET_assert (NULL != c); if (0 == strcmp ("question", cd->type)) { /* security question, answer must be a string */ json_t *janswer = json_object_get (sctx->args, "answer"); const char *answer = json_string_value (janswer); if (NULL == answer) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'answer' missing"); sctx_free (sctx); return; } /* persist answer, in case payment is required */ GNUNET_assert (0 == json_object_set (c, "answer", janswer)); ret = ANASTASIS_challenge_answer (ci, psp, timeout, answer, &answer_feedback_cb, sctx); } else { /* Check if we got a PIN or a HASH */ json_t *pin = json_object_get (sctx->args, "pin"); json_t *hash = json_object_get (sctx->args, "hash"); if (json_is_integer (pin)) { uint64_t ianswer = json_integer_value (pin); /* persist answer, in case async processing happens via poll */ GNUNET_assert (0 == json_object_set (c, "answer-pin", pin)); ret = ANASTASIS_challenge_answer2 (ci, psp, timeout, ianswer, &answer_feedback_cb, sctx); } else if (NULL != hash) { struct GNUNET_HashCode hashed_answer; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("hash", &hashed_answer), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (sctx->args, spec, NULL, NULL)) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'answer' malformed"); sctx_free (sctx); return; } ret = ANASTASIS_challenge_answer3 (ci, psp, timeout, &hashed_answer, &answer_feedback_cb, sctx); } else { /* no answer provided */ ret = ANASTASIS_challenge_start (ci, psp, &start_feedback_cb, sctx); } } if (GNUNET_OK != ret) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "Failed to begin answering challenge"); sctx_free (sctx); return; } return; /* await answer feedback */ } ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'uuid' not in list of challenges"); sctx_free (sctx); } /** * Callback which passes back the recovery document and its possible * policies. Also passes back the version of the document for the user * to check. * * We find the selected challenge and try to answer it (or begin * the process). * * @param cls a `struct SelectChallengeContext *` * @param ri recovery information struct which contains the policies */ static void pay_challenge_cb (void *cls, const struct ANASTASIS_RecoveryInformation *ri) { struct SelectChallengeContext *sctx = cls; json_t *challenge; if (NULL == ri) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "recovery information could not be deserialized"); sctx_free (sctx); return; } challenge = find_challenge_in_ri (sctx->state, &sctx->uuid); if (NULL == challenge) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "challenge not found"); sctx_free (sctx); return; } /* persist payment, in case we need to run the request again */ GNUNET_assert ( 0 == json_object_set_new (challenge, "payment_secret", GNUNET_JSON_from_data_auto (&sctx->ps))); for (unsigned int i = 0; ics_len; i++) { struct ANASTASIS_Challenge *ci = ri->cs[i]; const struct ANASTASIS_ChallengeDetails *cd; int ret; cd = ANASTASIS_challenge_get_details (ci); if (0 != GNUNET_memcmp (&sctx->uuid, &cd->uuid)) continue; if (cd->solved) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "Selected challenge already solved"); sctx_free (sctx); return; } if (0 == strcmp ("question", cd->type)) { /* security question, answer must be a string and already ready */ json_t *janswer = json_object_get (challenge, "answer"); const char *answer = json_string_value (janswer); if (NULL == answer) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'answer' missing"); sctx_free (sctx); return; } ret = ANASTASIS_challenge_answer (ci, &sctx->ps, sctx->timeout, answer, &answer_feedback_cb, sctx); } else { ret = ANASTASIS_challenge_start (ci, &sctx->ps, &start_feedback_cb, sctx); } if (GNUNET_OK != ret) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "Failed to begin answering challenge"); sctx_free (sctx); return; } return; /* await answer feedback */ } ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'uuid' not in list of challenges"); sctx_free (sctx); } /** * The user selected a challenge to be solved. Begin the solving * process. * * @param[in] state we are in * @param arguments our arguments with the solution * @param cb functiont o call with the new state * @param cb_cls closure for @a cb * @return handle to cancel challenge selection step */ static struct ANASTASIS_ReduxAction * solve_challenge (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { struct SelectChallengeContext *sctx = GNUNET_new (struct SelectChallengeContext); json_t *rd; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("selected_challenge_uuid", &sctx->uuid), GNUNET_JSON_spec_end () }; if (NULL == arguments) { ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "arguments missing"); return NULL; } if (GNUNET_OK != GNUNET_JSON_parse (state, spec, NULL, NULL)) { ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'selected_challenge_uuid' missing"); return NULL; } rd = json_object_get (state, "recovery_document"); if (NULL == rd) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "solve_challenge"); return NULL; } sctx->cb = cb; sctx->cb_cls = cb_cls; sctx->state = json_incref (state); sctx->args = json_incref ((json_t*) arguments); sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, rd, &solve_challenge_cb, sctx, &core_secret_cb, sctx); if (NULL == sctx->r) { json_decref (sctx->state); json_decref (sctx->args); GNUNET_free (sctx); GNUNET_break_op (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'recovery_document' invalid"); return NULL; } sctx->ra.cleanup = &sctx_free; sctx->ra.cleanup_cls = sctx; return &sctx->ra; } /** * The user asked for us to poll on pending * asynchronous challenges to see if they have * now completed / been satisfied. * * @param[in] state we are in * @param arguments our arguments with the solution * @param cb functiont o call with the new state * @param cb_cls closure for @a cb * @return handle to cancel challenge selection step */ static struct ANASTASIS_ReduxAction * poll_challenges (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { struct SelectChallengeContext *sctx = GNUNET_new (struct SelectChallengeContext); json_t *rd; rd = json_object_get (state, "recovery_document"); if (NULL == rd) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "poll_challenges"); return NULL; } sctx->poll_only = true; sctx->cb = cb; sctx->cb_cls = cb_cls; sctx->state = json_incref (state); sctx->args = json_incref ((json_t*) arguments); sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, rd, &solve_challenge_cb, sctx, &core_secret_cb, sctx); if (NULL == sctx->r) { json_decref (sctx->state); json_decref (sctx->args); GNUNET_free (sctx); GNUNET_break_op (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'recovery_document' invalid"); return NULL; } sctx->ra.cleanup = &sctx_free; sctx->ra.cleanup_cls = sctx; return &sctx->ra; } /** * The user selected a challenge to be solved. Handle the payment * process. * * @param[in] state we are in * @param arguments our arguments with the solution * @param cb functiont o call with the new state * @param cb_cls closure for @a cb * @return handle to cancel challenge selection step */ static struct ANASTASIS_ReduxAction * pay_challenge (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { struct SelectChallengeContext *sctx = GNUNET_new (struct SelectChallengeContext); json_t *rd; struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("selected_challenge_uuid", &sctx->uuid), GNUNET_JSON_spec_end () }; struct GNUNET_JSON_Specification aspec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_relative_time ("timeout", &timeout), NULL), GNUNET_JSON_spec_fixed_auto ("payment_secret", &sctx->ps), GNUNET_JSON_spec_end () }; if (NULL == arguments) { ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "arguments missing"); return NULL; } if (GNUNET_OK != GNUNET_JSON_parse (arguments, aspec, NULL, NULL)) { ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'payment_secret' missing"); return NULL; } if (GNUNET_OK != GNUNET_JSON_parse (state, spec, NULL, NULL)) { ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'selected_challenge_uuid' missing"); return NULL; } rd = json_object_get (state, "recovery_document"); if (NULL == rd) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "pay_challenge"); return NULL; } sctx->timeout = timeout; sctx->cb = cb; sctx->cb_cls = cb_cls; sctx->state = json_incref (state); sctx->args = json_incref ((json_t*) arguments); sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, rd, &pay_challenge_cb, sctx, &core_secret_cb, sctx); if (NULL == sctx->r) { json_decref (sctx->state); json_decref (sctx->args); GNUNET_free (sctx); GNUNET_break_op (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'recovery_document' invalid"); return NULL; } sctx->ra.cleanup = &sctx_free; sctx->ra.cleanup_cls = sctx; return &sctx->ra; } /** * Callback which passes back the recovery document and its possible * policies. Also passes back the version of the document for the user * to check. * * We find the selected challenge and try to answer it (or begin * the process). * * @param cls a `struct SelectChallengeContext *` * @param ri recovery information struct which contains the policies */ static void select_challenge_cb (void *cls, const struct ANASTASIS_RecoveryInformation *ri) { struct SelectChallengeContext *sctx = cls; const struct ANASTASIS_PaymentSecretP *psp = NULL; struct ANASTASIS_PaymentSecretP ps; struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; struct GNUNET_JSON_Specification tspec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_relative_time ("timeout", &timeout), NULL), GNUNET_JSON_spec_end () }; struct GNUNET_JSON_Specification pspec[] = { GNUNET_JSON_spec_fixed_auto ("payment_secret", &ps), GNUNET_JSON_spec_end () }; if (NULL == ri) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "recovery information could not be deserialized"); sctx_free (sctx); return; } if (GNUNET_OK != GNUNET_JSON_parse (sctx->args, tspec, NULL, NULL)) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'timeout' malformed"); sctx_free (sctx); return; } /* NOTE: do we need both ways to pass payment secrets? */ if (NULL != json_object_get (sctx->args, "payment_secret")) { /* check if we got payment secret in args */ if (GNUNET_OK != GNUNET_JSON_parse (sctx->args, pspec, NULL, NULL)) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'payment_secret' malformed"); sctx_free (sctx); return; } psp = &ps; } else { /* Check if we got a payment_secret in state */ json_t *challenge = find_challenge_in_ri (sctx->state, &sctx->uuid); if (NULL == challenge) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "challenge not found"); sctx_free (sctx); return; } if (NULL != json_object_get (challenge, "payment_secret")) { if (GNUNET_OK != GNUNET_JSON_parse (challenge, pspec, NULL, NULL)) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'payment_secret' malformed"); sctx_free (sctx); return; } psp = &ps; } } for (unsigned int i = 0; ics_len; i++) { struct ANASTASIS_Challenge *ci = ri->cs[i]; const struct ANASTASIS_ChallengeDetails *cd; int ret; cd = ANASTASIS_challenge_get_details (ci); if (0 != GNUNET_memcmp (&sctx->uuid, &cd->uuid)) continue; if (cd->solved) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "Selected challenge already solved"); sctx_free (sctx); return; } GNUNET_assert ( 0 == json_object_set_new (sctx->state, "selected_challenge_uuid", GNUNET_JSON_from_data_auto (&cd->uuid))); if ( (0 == strcmp ("question", cd->type)) || (0 == strcmp ("totp", cd->type)) ) { /* security question or TOTP: immediately request user to answer it */ set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); sctx->cb (sctx->cb_cls, TALER_EC_NONE, sctx->state); sctx_free (sctx); return; } /* trigger challenge */ { json_t *c = find_challenge_in_cs (sctx->state, &cd->uuid); json_t *pin = json_object_get (c, "answer-pin"); if (NULL != pin) { uint64_t ianswer = json_integer_value (pin); ret = ANASTASIS_challenge_answer2 (ci, psp, timeout, ianswer, &answer_feedback_cb, sctx); } else { ret = ANASTASIS_challenge_start (ci, psp, &start_feedback_cb, sctx); } } if (GNUNET_OK != ret) { ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "Failed to begin answering challenge"); sctx_free (sctx); return; } return; /* await answer feedback */ } ANASTASIS_redux_fail_ (sctx->cb, sctx->cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'uuid' not in list of challenges"); sctx_free (sctx); } /** * The user selected a challenge to be solved. Begin the solving * process. * * @param[in] state we are in * @param arguments our arguments with the solution * @param cb functiont o call with the new state * @param cb_cls closure for @a cb * @return handle to cancel challenge selection step */ static struct ANASTASIS_ReduxAction * select_challenge (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { struct SelectChallengeContext *sctx = GNUNET_new (struct SelectChallengeContext); json_t *rd; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("uuid", &sctx->uuid), GNUNET_JSON_spec_end () }; if (NULL == arguments) { ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "arguments missing"); return NULL; } if (GNUNET_OK != GNUNET_JSON_parse (arguments, spec, NULL, NULL)) { ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'uuid' missing"); return NULL; } rd = json_object_get (state, "recovery_document"); if (NULL == rd) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "select_challenge"); return NULL; } sctx->cb = cb; sctx->cb_cls = cb_cls; sctx->state = json_incref (state); sctx->args = json_incref ((json_t*) arguments); sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, rd, &select_challenge_cb, sctx, &core_secret_cb, sctx); if (NULL == sctx->r) { json_decref (sctx->state); json_decref (sctx->args); GNUNET_free (sctx); GNUNET_break_op (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'recovery_document' invalid"); return NULL; } sctx->ra.cleanup = &sctx_free; sctx->ra.cleanup_cls = sctx; return &sctx->ra; } /** * Main data structure for sync_providers(). */ struct MasterSync; /** * Data structure for one provider we are syncing /config with. */ struct SyncEntry { /** * Kept in a DLL. */ struct SyncEntry *next; /** * Kept in a DLL. */ struct SyncEntry *prev; /** * Sync operation we are part of. */ struct MasterSync *ms; /** * Redux action for this provider. */ struct ANASTASIS_ReduxAction *ra; }; /** * Main data structure for sync_providers(). */ struct MasterSync { /** * Our own sync action we expose externally. */ struct ANASTASIS_ReduxAction ra; /** * Head of DLL with entries per provider. */ struct SyncEntry *se_head; /** * Tail of DLL with entries per provider. */ struct SyncEntry *se_tail; /** * Function to call with the result. */ ANASTASIS_ActionCallback cb; /** * Closure for @e cb. */ void *cb_cls; }; /** * Free @a cls data structure. * * @param[in] cls data structure to free, must be a `struct MasterSync *` */ static void clean_sync (void *cls) { struct MasterSync *ms = cls; struct SyncEntry *se; while (NULL != (se = ms->se_head)) { GNUNET_CONTAINER_DLL_remove (ms->se_head, ms->se_tail, se); se->ra->cleanup (se->ra->cleanup_cls); GNUNET_free (se); } GNUNET_free (ms); } /** * Function called when we have made progress on any of the * providers we are trying to sync with. * * @param cls closure * @param error error code, #TALER_EC_NONE if @a new_bs is the new successful state * @param new_state the new state of the operation (client should json_incref() to keep an alias) */ static void sync_progress (void *cls, enum TALER_ErrorCode error, json_t *new_state) { struct SyncEntry *se = cls; struct MasterSync *ms = se->ms; GNUNET_CONTAINER_DLL_remove (ms->se_head, ms->se_tail, se); GNUNET_free (se); ms->cb (ms->cb_cls, error, new_state); clean_sync (ms); } /** * Check if we have information on all providers involved in * a recovery procedure, and if not, try to obtain it. Upon * success, call @a cb with the updated provider status data. * * @param[in] state we are in * @param arguments our arguments with the solution * @param cb functiont o call with the new state * @param cb_cls closure for @a cb * @return handle to cancel challenge selection step */ static struct ANASTASIS_ReduxAction * sync_providers (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { json_t *rd; json_t *cs_arr; struct MasterSync *ms; rd = json_object_get (state, "recovery_document"); if (NULL == rd) { GNUNET_break (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'recovery_document' missing"); return NULL; } cs_arr = json_object_get (rd, "challenges"); if (! json_is_array (cs_arr)) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'recovery_document' must be an array"); return NULL; } ms = GNUNET_new (struct MasterSync); ms->cb = cb; ms->cb_cls = cb_cls; { json_t *cs; unsigned int n_index; json_array_foreach (cs_arr, n_index, cs) { const char *provider_url; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("url", &provider_url), GNUNET_JSON_spec_end () }; struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt; struct SyncEntry *se; if (GNUNET_OK != GNUNET_JSON_parse (cs, spec, NULL, NULL)) { GNUNET_break_op (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'recovery_document' missing"); clean_sync (ms); return NULL; } if (GNUNET_OK == ANASTASIS_reducer_lookup_salt (state, provider_url, &provider_salt)) continue; /* provider already ready */ se = GNUNET_new (struct SyncEntry); se->ms = ms; GNUNET_CONTAINER_DLL_insert (ms->se_head, ms->se_tail, se); se->ra = ANASTASIS_REDUX_add_provider_to_state_ (provider_url, state, &sync_progress, se); } } if (NULL == ms->se_head) { /* everything already synced */ clean_sync (ms); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_PROVIDERS_ALREADY_SYNCED, "already in sync"); return NULL; } ms->ra.cleanup = &clean_sync; ms->ra.cleanup_cls = ms; return &ms->ra; } /** * Try to obtain configuration information on all configured * providers. Upon success, call @a cb with the updated provider * status data. * * @param[in] state we are in * @param arguments our arguments with the solution * @param cb functiont o call with the new state * @param cb_cls closure for @a cb * @return handle to cancel challenge selection step */ static struct ANASTASIS_ReduxAction * poll_providers (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { json_t *ap; const char *url; json_t *obj; struct MasterSync *ms; ap = json_object_get (state, "authentication_providers"); if (NULL == ap) { GNUNET_break (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'authentication_providers' missing"); return NULL; } ms = GNUNET_new (struct MasterSync); ms->cb = cb; ms->cb_cls = cb_cls; json_object_foreach (ap, url, obj) { struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt; struct SyncEntry *se; struct ANASTASIS_ReduxAction *ra; if (GNUNET_OK == ANASTASIS_reducer_lookup_salt (state, url, &provider_salt)) continue; se = GNUNET_new (struct SyncEntry); se->ms = ms; GNUNET_CONTAINER_DLL_insert (ms->se_head, ms->se_tail, se); ra = ANASTASIS_REDUX_add_provider_to_state_ (url, state, &sync_progress, se); if (NULL == ra) return NULL; /* sync_progress already called! */ se->ra = ra; } if (NULL == ms->se_head) { /* everything already synced */ clean_sync (ms); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID, "already in sync"); return NULL; } ms->ra.cleanup = &clean_sync; ms->ra.cleanup_cls = ms; return &ms->ra; } /** * The user pressed "back" during challenge solving. * Transition back to selecting another challenge. * * @param[in] state we are in * @param arguments our arguments (unused) * @param cb functiont o call with the new state * @param cb_cls closure for @a cb * @return NULL (synchronous operation) */ static struct ANASTASIS_ReduxAction * back_challenge_solving (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { (void) arguments; GNUNET_assert (0 == json_object_del (state, "selected_challenge_uuid")); set_state (state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); cb (cb_cls, TALER_EC_NONE, state); return NULL; } /** * State for a "policy download" as part of a recovery operation. */ struct PolicyDownloadEntry { /** * Redux action handle associated with this state. */ struct ANASTASIS_ReduxAction ra; /** * Backend we are querying. */ char *backend_url; /** * The /policy GET operation handle. */ struct ANASTASIS_Recovery *recovery; /** * Function to call with the result. */ ANASTASIS_ActionCallback cb; /** * Closure for @e cb. */ void *cb_cls; /** * State we are using. */ json_t *state; }; /** * Free @a cls data structure. * * @param[in] cls data structure to free, must be a `struct PolicyDownloadEntry *` */ static void free_pd (void *cls) { struct PolicyDownloadEntry *pd = cls; if (NULL != pd->recovery) { ANASTASIS_recovery_abort (pd->recovery); pd->recovery = NULL; } GNUNET_free (pd->backend_url); json_decref (pd->state); GNUNET_free (pd); } /** * We failed to download a policy. Show an error to the user and * allow the user to specify alternative providers and/or policy * versions. * * @param[in] pd state to fail with the policy download * @param offline true of the reason to show is that all providers * were offline / did not return a salt to us */ static void return_no_policy (struct PolicyDownloadEntry *pd, bool offline) { enum TALER_ErrorCode ec = TALER_EC_ANASTASIS_REDUCER_NETWORK_FAILED; const char *detail = (offline) ? "could not contact provider (offline)" : "provider does not know this policy"; json_t *estate; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Provider offline!\n"); estate = GNUNET_JSON_PACK ( GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("detail", detail)), GNUNET_JSON_pack_uint64 ("code", ec), GNUNET_JSON_pack_string ("hint", TALER_ErrorCode_get_hint (ec))); pd->cb (pd->cb_cls, ec, estate); free_pd (pd); } /** * Callback which passes back the recovery document and its possible * policies. Also passes back the version of the document for the user * to check. * * Once the first policy lookup succeeds, we update our state and * cancel all of the others, passing the obtained recovery information * back to the user. * * @param cls closure for the callback with a `struct PolicyDownloadEntry *` * @param ri recovery information struct which contains the policies */ static void policy_lookup_cb (void *cls, const struct ANASTASIS_RecoveryInformation *ri) { struct PolicyDownloadEntry *pd = cls; json_t *policies; json_t *challenges; json_t *recovery_information; if (NULL == ri) { /* Woopsie, failed hard. */ ANASTASIS_recovery_abort (pd->recovery); GNUNET_free (pd->backend_url); GNUNET_free (pd); return_no_policy (pd, false); return; } policies = json_array (); GNUNET_assert (NULL != policies); for (unsigned int i = 0; idps_len; i++) { struct ANASTASIS_DecryptionPolicy *dps = ri->dps[i]; json_t *pchallenges; pchallenges = json_array (); GNUNET_assert (NULL != pchallenges); for (unsigned int j = 0; jchallenges_length; j++) { struct ANASTASIS_Challenge *c = dps->challenges[j]; const struct ANASTASIS_ChallengeDetails *cd; json_t *cj; cd = ANASTASIS_challenge_get_details (c); cj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("uuid", &cd->uuid)); GNUNET_assert (0 == json_array_append_new (pchallenges, cj)); } GNUNET_assert (0 == json_array_append_new (policies, pchallenges)); } /* end for all policies */ challenges = json_array (); GNUNET_assert (NULL != challenges); for (unsigned int i = 0; ics_len; i++) { struct ANASTASIS_Challenge *c = ri->cs[i]; const struct ANASTASIS_ChallengeDetails *cd; json_t *cj; cd = ANASTASIS_challenge_get_details (c); cj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("uuid", &cd->uuid), GNUNET_JSON_pack_string ("type", cd->type), GNUNET_JSON_pack_string ("uuid-display", ANASTASIS_CRYPTO_uuid2s (&cd->uuid)), GNUNET_JSON_pack_string ("instructions", cd->instructions)); GNUNET_assert (0 == json_array_append_new (challenges, cj)); } /* end for all challenges */ recovery_information = GNUNET_JSON_PACK ( GNUNET_JSON_pack_array_steal ("challenges", challenges), GNUNET_JSON_pack_array_steal ("policies", policies), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("secret_name", ri->secret_name)), GNUNET_JSON_pack_string ("provider_url", pd->backend_url), GNUNET_JSON_pack_uint64 ("version", ri->version)); GNUNET_assert (0 == json_object_set_new (pd->state, "recovery_information", recovery_information)); { json_t *rd; rd = ANASTASIS_recovery_serialize (pd->recovery); if (NULL == rd) { GNUNET_break (0); ANASTASIS_redux_fail_ (pd->cb, pd->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "unable to serialize recovery state"); free_pd (pd); return; } GNUNET_assert (0 == json_object_set_new (pd->state, "recovery_document", rd)); } set_state (pd->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); pd->cb (pd->cb_cls, TALER_EC_NONE, pd->state); free_pd (pd); } /** * This function is called whenever the recovery process ends. * In this case, that should not be possible as this callback * is used before we even begin with the challenges. So if * we are called, it is because of some fatal error. * * @param cls a `struct PolicyDownloadEntry` * @param rc error code * @param secret contains the core secret which is passed to the user * @param secret_size defines the size of the core secret */ static void core_early_secret_cb (void *cls, enum ANASTASIS_RecoveryStatus rc, const void *secret, size_t secret_size) { struct PolicyDownloadEntry *pd = cls; pd->recovery = NULL; GNUNET_assert (NULL == secret); GNUNET_assert (ANASTASIS_RS_SUCCESS != rc); fail_by_error (pd->cb, pd->cb_cls, rc); free_pd (pd); } /** * DispatchHandler/Callback function which is called for a * "next" action in "secret_selecting" state. * * @param state state to operate on * @param arguments arguments to use for operation on state * @param cb callback to call during/after operation * @param cb_cls callback closure * @return NULL */ static struct ANASTASIS_ReduxAction * done_secret_selecting (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { uint32_t mask; json_t *pa; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_uint32 ("attribute_mask", &mask), GNUNET_JSON_spec_json ("providers", &pa), GNUNET_JSON_spec_end () }; struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt; struct GNUNET_JSON_Specification pspec[] = { GNUNET_JSON_spec_fixed_auto ("provider_salt", &provider_salt), GNUNET_JSON_spec_end () }; json_t *p_cfg; json_t *id_data; const json_t *providers; if (GNUNET_OK != GNUNET_JSON_parse (arguments, spec, NULL, NULL)) { GNUNET_break (0); json_dumpf (arguments, stderr, JSON_INDENT (2)); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, NULL); return NULL; } providers = json_object_get (state, "authentication_providers"); if ( (NULL == providers) || (! json_is_object (providers)) ) { GNUNET_break (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'authentication_providers' missing"); return NULL; } { size_t poff; json_t *pe; uint64_t version; const char *provider_url; json_array_foreach (pa, poff, pe) { struct GNUNET_JSON_Specification ispec[] = { GNUNET_JSON_spec_uint64 ("version", &version), GNUNET_JSON_spec_string ("url", &provider_url), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (pe, ispec, NULL, NULL)) { GNUNET_break (0); json_dumpf (pe, stderr, JSON_INDENT (2)); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, NULL); return NULL; } p_cfg = json_object_get (providers, provider_url); if (MHD_HTTP_OK != json_integer_value (json_object_get (p_cfg, "http_status"))) continue; if (GNUNET_OK != GNUNET_JSON_parse (p_cfg, pspec, NULL, NULL)) { GNUNET_break (0); /* should be impossible for well-formed state */ ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "Salt unknown for selected provider"); return NULL; } id_data = json_object_get (state, "identity_attributes"); if (NULL == id_data) { GNUNET_break (0); /* should be impossible for well-formed state */ ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'identity_attributes' missing"); return NULL; } { struct PolicyDownloadEntry *pd = GNUNET_new (struct PolicyDownloadEntry); pd->cb = cb; pd->cb_cls = cb_cls; pd->state = json_incref (state); pd->backend_url = GNUNET_strdup (provider_url); pd->recovery = ANASTASIS_recovery_begin (ANASTASIS_REDUX_ctx_, id_data, version, pd->backend_url, &provider_salt, &policy_lookup_cb, pd, &core_early_secret_cb, pd); if (NULL == pd->recovery) { GNUNET_break (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INTERNAL_ERROR, NULL); GNUNET_free (pd->backend_url); json_decref (pd->state); GNUNET_free (pd); return NULL; } pd->ra.cleanup = &free_pd; pd->ra.cleanup_cls = pd; return &pd->ra; } } } /* no provider worked */ ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "selected provider is not online"); return NULL; } /** * The user wants us to add another provider. Download /config. * * @param[in] state we are in * @param arguments our arguments with the solution * @param cb function to call with the new state * @param cb_cls closure for @a cb * @return handle to cancel challenge selection step */ static struct ANASTASIS_ReduxAction * add_provider (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { const char *provider_url; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("provider_url", &provider_url), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (arguments, spec, NULL, NULL)) { GNUNET_break (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, NULL); return NULL; } return ANASTASIS_REDUX_add_provider_to_state_ (provider_url, state, cb, cb_cls); } /** * Signature of callback function that implements a state transition. * * @param state current state * @param arguments arguments for the state transition * @param cb function to call when done * @param cb_cls closure for @a cb */ typedef struct ANASTASIS_ReduxAction * (*DispatchHandler)(json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls); struct ANASTASIS_ReduxAction * ANASTASIS_recovery_action_ (json_t *state, const char *action, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { struct Dispatcher { enum ANASTASIS_RecoveryState recovery_state; const char *recovery_action; DispatchHandler fun; } dispatchers[] = { { ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, "add_provider", &add_provider }, { ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, "poll_providers", &poll_providers }, { ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, "select_version", &done_secret_selecting }, { ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, "back", &ANASTASIS_back_generic_decrement_ }, { ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING, "select_challenge", &select_challenge }, { ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING, "sync_providers", &sync_providers }, { ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING, "poll", &poll_challenges }, { ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING, "back", &ANASTASIS_back_generic_decrement_ }, { ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING, "pay", &pay_challenge }, { ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING, "back", &ANASTASIS_back_generic_decrement_ }, { ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING, "solve_challenge", &solve_challenge }, { ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING, "back", &back_challenge_solving }, { ANASTASIS_RECOVERY_STATE_INVALID, NULL, NULL } }; const char *s = json_string_value (json_object_get (state, "recovery_state")); enum ANASTASIS_RecoveryState rs; GNUNET_assert (NULL != s); rs = ANASTASIS_recovery_state_from_string_ (s); if (ANASTASIS_RECOVERY_STATE_INVALID == rs) { ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'recovery_state' field invalid"); return NULL; } for (unsigned int i = 0; NULL != dispatchers[i].fun; i++) { if ( (rs == dispatchers[i].recovery_state) && (0 == strcmp (action, dispatchers[i].recovery_action)) ) { return dispatchers[i].fun (state, arguments, cb, cb_cls); } } ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID, action); return NULL; } struct ANASTASIS_ReduxAction * ANASTASIS_REDUX_recovery_challenge_begin_ (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { const json_t *providers; json_t *attributes; providers = json_object_get (state, "authentication_providers"); if ( (NULL == providers) || (! json_is_object (providers)) ) { GNUNET_break (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'authentication_providers' missing"); return NULL; } attributes = json_object_get (arguments, "identity_attributes"); if ( (NULL == attributes) || (! json_is_object (attributes)) ) { GNUNET_break (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "'identity_attributes' missing"); return NULL; } json_object_set (state, "identity_attributes", attributes); set_state (state, ANASTASIS_RECOVERY_STATE_SECRET_SELECTING); cb (cb_cls, TALER_EC_NONE, state); return NULL; }