/* 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 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 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_ERROR; } 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; (void) cfg; initial_state = ANASTASIS_REDUX_load_continents_ (); if (NULL == initial_state) return NULL; 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); } /** * Update @a state to reflect the error provided in @a rc. * * @param[in,out] state state to update * @param rc error code to translate to JSON * @return error code to use */ static enum TALER_ErrorCode update_state_by_error (json_t *state, 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; } GNUNET_assert (0 == json_object_set_new (state, "error_message", json_string (msg))); GNUNET_assert (0 == json_object_set_new (state, "error_code", json_integer (rc))); set_state (state, ANASTASIS_RECOVERY_STATE_ERROR); return ec; } /** * 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; enum TALER_ErrorCode ec; 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; } ec = update_state_by_error (sctx->state, rc); sctx->cb (sctx->cb_cls, ec, sctx->state); 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 "cs". * * @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, "cs"); 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 answer_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_STATUS_SOLVED: { json_t *rd; rd = ANASTASIS_recovery_serialize (sctx->r); if (NULL == rd) { GNUNET_break (0); set_state (sctx->state, ANASTASIS_RECOVERY_STATE_ERROR); sctx->cb (sctx->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, sctx->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_STATUS_INSTRUCTIONS: { json_t *instructions; const char *mime; mime = csr->details.open_challenge.content_type; if (NULL != mime) { if ( (0 == strcasecmp (mime, "text/plain")) || (0 == strcasecmp (mime, "text/utf8")) ) { char *s = GNUNET_strndup (csr->details.open_challenge.body, csr->details.open_challenge.body_size); instructions = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "hint"), GNUNET_JSON_pack_string ("hint", s), GNUNET_JSON_pack_uint64 ("http_status", (json_int_t) csr->details.open_challenge. http_status)); GNUNET_free (s); } else if (0 == strcasecmp (mime, "application/json")) { json_t *body; body = json_loadb (csr->details.open_challenge.body, csr->details.open_challenge.body_size, JSON_REJECT_DUPLICATES, NULL); if (NULL == body) { GNUNET_break_op (0); mime = NULL; } else { instructions = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "details"), GNUNET_JSON_pack_object_steal ("details", body), GNUNET_JSON_pack_uint64 ("http_status", csr->details.open_challenge.http_status)); } } else { /* unexpected / unsupported mime type */ mime = NULL; } } if (NULL == mime) { instructions = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "body"), GNUNET_JSON_pack_data_varsize ("body", csr->details.open_challenge.body, csr->details.open_challenge.body_size), GNUNET_JSON_pack_uint64 ("http_status", csr->details.open_challenge.http_status), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("mime_type", mime))); } 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_STATUS_REDIRECT_FOR_AUTHENTICATION: { json_t *redir; redir = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "redirect"), GNUNET_JSON_pack_string ("redirect_url", csr->details.redirect_url)); GNUNET_assert (NULL != redir); GNUNET_assert (0 == json_object_set_new (feedback, uuid, redir)); } 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_STATUS_PAYMENT_REQUIRED: { json_t *pay; pay = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "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_data_auto ( "payment_secret", &csr->details.payment_required.payment_secret)); 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_STATUS_SERVER_FAILURE: { json_t *err; err = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "server-failure"), GNUNET_JSON_pack_uint64 ("http_status", csr->details.server_failure. http_status), GNUNET_JSON_pack_uint64 ("error_code", csr->details.server_failure.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->details.server_failure.ec, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_STATUS_TRUTH_UNKNOWN: { json_t *err; err = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "truth-unknown"), 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_STATUS_RATE_LIMIT_EXCEEDED: { json_t *err; err = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "rate-limit-exceeded"), GNUNET_JSON_pack_uint64 ("error_code", TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED)); 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; case ANASTASIS_CHALLENGE_STATUS_AUTH_TIMEOUT: { json_t *err; err = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "authentication-timeout"), GNUNET_JSON_pack_uint64 ("error_code", TALER_EC_ANASTASIS_TRUTH_AUTH_TIMEOUT)); GNUNET_assert (0 == json_object_set_new (feedback, uuid, err)); } GNUNET_break_op (0); set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); sctx->cb (sctx->cb_cls, TALER_EC_ANASTASIS_TRUTH_AUTH_TIMEOUT, sctx->state); sctx_free (sctx); return; case ANASTASIS_CHALLENGE_STATUS_EXTERNAL_INSTRUCTIONS: { const json_t *body = csr->details.external_challenge; const char *method; json_t *details; bool is_async = false; uint64_t code = 0; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("method", &method), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_bool ("async", &is_async)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint64 ("answer_code", &code)), GNUNET_JSON_spec_json ("details", &details), GNUNET_JSON_spec_end () }; json_t *reply; if (GNUNET_OK != GNUNET_JSON_parse (body, spec, NULL, NULL)) { json_t *err; GNUNET_break_op (0); err = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "server-failure"), GNUNET_JSON_pack_uint64 ("error_code", TALER_EC_GENERIC_REPLY_MALFORMED)); GNUNET_assert (0 == json_object_set_new (feedback, uuid, err)); return; } if (is_async) { json_t *c = find_challenge_in_cs (sctx->state, &cd->uuid); if (NULL == c) { GNUNET_break (0); set_state (sctx->state, ANASTASIS_RECOVERY_STATE_ERROR); sctx->cb (sctx->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, sctx->state); 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 (code))); } reply = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("state", "external-instructions"), GNUNET_JSON_pack_string ("method", method), GNUNET_JSON_pack_object_incref ("details", details)); GNUNET_JSON_parse_free (spec); GNUNET_assert (0 == json_object_set_new (feedback, uuid, reply)); } 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); } /** * 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)), 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_start (ci, psp, timeout, &hashed_answer, &answer_feedback_cb, sctx); } else { /* no answer provided */ ret = ANASTASIS_challenge_start (ci, psp, timeout, NULL, /* no answer */ &answer_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, sctx->timeout, NULL, /* no answer yet */ &answer_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)), 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)), 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)) { /* security question, 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, timeout, NULL, /* no answer */ &answer_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; } /** * 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; } /** * The user wants us to change the policy version. Download another version. * * @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 * change_version (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { uint64_t version; const char *provider_url; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_uint64 ("version", &version), GNUNET_JSON_spec_string ("provider_url", &provider_url), GNUNET_JSON_spec_end () }; json_t *ia; json_t *args; struct ANASTASIS_ReduxAction *ra; 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, "'version' invalid"); return NULL; } GNUNET_assert (NULL != provider_url); ia = json_object_get (state, "identity_attributes"); if (NULL == ia) { GNUNET_break (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, "'identity_attributes' missing"); return NULL; } args = GNUNET_JSON_PACK ( GNUNET_JSON_pack_uint64 ("version", version), GNUNET_JSON_pack_object_incref ("identity_attributes", (json_t *) ia), GNUNET_JSON_pack_string ("provider_url", provider_url)); if (NULL == args) { GNUNET_break (0); ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, NULL); return NULL; } ra = ANASTASIS_REDUX_recovery_challenge_begin_ (state, args, cb, cb_cls); json_decref (args); return ra; } /** * 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) { const json_t *ri; ri = json_object_get (state, "recovery_information"); if ( (NULL == ri) || (NULL == json_object_get (ri, "challenges")) ) { ANASTASIS_redux_fail_ (cb, cb_cls, TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, "no valid version selected"); return NULL; } set_state (state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); cb (cb_cls, TALER_EC_NONE, state); return NULL; } /** * 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, "change_version", &change_version }, { ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, "next", &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, "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_ERROR, 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_ERROR == 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; } /** * State for a "recover secret" CMD. */ struct RecoverSecretState; /** * State for a "policy download" as part of a recovery operation. */ struct PolicyDownloadEntry { /** * Kept in a DLL. */ struct PolicyDownloadEntry *prev; /** * Kept in a DLL. */ struct PolicyDownloadEntry *next; /** * Backend we are querying. */ char *backend_url; /** * Salt to be used to derive the id for this provider */ struct ANASTASIS_CRYPTO_ProviderSaltP salt; /** * Context we operate in. */ struct RecoverSecretState *rss; /** * The /policy GET operation handle. */ struct ANASTASIS_Recovery *recovery; }; /** * Entry in the list of all known applicable Anastasis providers. * Used to wait for it to complete downloading /config. */ struct RecoveryStartStateProviderEntry { /** * Kept in a DLL. */ struct RecoveryStartStateProviderEntry *next; /** * Kept in a DLL. */ struct RecoveryStartStateProviderEntry *prev; /** * Main operation this entry is part of. */ struct RecoverSecretState *rss; /** * Resulting provider information, NULL if not (yet) available. */ json_t *istate; /** * Ongoing reducer action to obtain /config, NULL if completed. */ struct ANASTASIS_ReduxAction *ra; /** * Final result of the operation (once completed). */ enum TALER_ErrorCode ec; }; /** * State for a "recover secret" CMD. */ struct RecoverSecretState { /** * Redux action handle associated with this state. */ struct ANASTASIS_ReduxAction ra; /** * Head of list of provider /config operations we are doing. */ struct RecoveryStartStateProviderEntry *pe_head; /** * Tail of list of provider /config operations we are doing. */ struct RecoveryStartStateProviderEntry *pe_tail; /** * Identification data from the user */ json_t *id_data; /** * Head of DLL of policy downloads. */ struct PolicyDownloadEntry *pd_head; /** * Tail of DLL of policy downloads. */ struct PolicyDownloadEntry *pd_tail; /** * Reference to our state. */ json_t *state; /** * callback to call during/after operation */ ANASTASIS_ActionCallback cb; /** * closure for action callback @e cb. */ void *cb_cls; /** * Set if recovery must be done with this provider. */ char *provider_url; /** * version of the recovery document to request. */ unsigned int version; /** * Number of provider /config operations in @e ba_head that * are still awaiting completion. */ unsigned int pending; /** * Is @e version set? */ bool have_version; }; /** * Function to free a `struct RecoverSecretState` * * @param cls must be a `struct RecoverSecretState` */ static void free_rss (void *cls) { struct RecoverSecretState *rss = cls; struct PolicyDownloadEntry *pd; struct RecoveryStartStateProviderEntry *pe; while (NULL != (pe = rss->pe_head)) { GNUNET_CONTAINER_DLL_remove (rss->pe_head, rss->pe_tail, pe); ANASTASIS_redux_action_cancel (pe->ra); rss->pending--; GNUNET_free (pe); } while (NULL != (pd = rss->pd_head)) { GNUNET_CONTAINER_DLL_remove (rss->pd_head, rss->pd_tail, pd); if (NULL != pd->recovery) { ANASTASIS_recovery_abort (pd->recovery); pd->recovery = NULL; } GNUNET_free (pd->backend_url); GNUNET_free (pd); } json_decref (rss->state); json_decref (rss->id_data); GNUNET_assert (0 == rss->pending); GNUNET_free (rss->provider_url); GNUNET_free (rss); } /** * 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; struct RecoverSecretState *rss = pd->rss; enum TALER_ErrorCode ec; pd->recovery = NULL; GNUNET_assert (NULL == secret); GNUNET_CONTAINER_DLL_remove (rss->pd_head, rss->pd_tail, pd); GNUNET_free (pd->backend_url); GNUNET_free (pd); if (NULL != rss->pd_head) return; /* wait for another one */ /* all failed! report failure! */ GNUNET_assert (ANASTASIS_RS_SUCCESS != rc); ec = update_state_by_error (rss->state, rc); rss->cb (rss->cb_cls, ec, rss->state); rss->cb = NULL; free_rss (rss); } /** * Determine recovery @a cost of solving a challenge of type @a type * at @a provider_url by inspecting @a state. * * @param state the state to inspect * @param provider_url the provider to lookup config info from * @param type the method to lookup the cost of * @param[out] cost the recovery cost to return * @return #GNUNET_OK on success, #GNUNET_NO if not found, #GNUNET_SYSERR on state error */ static int lookup_cost (const json_t *state, const char *provider_url, const char *type, struct TALER_Amount *cost) { const json_t *providers; const json_t *provider; const json_t *methods; providers = json_object_get (state, "authentication_providers"); if (NULL == providers) { GNUNET_break (0); return GNUNET_SYSERR; } provider = json_object_get (providers, provider_url); if (NULL == provider) { GNUNET_break (0); return GNUNET_SYSERR; } methods = json_object_get (provider, "methods"); if ( (NULL == methods) || (! json_is_array (methods)) ) { GNUNET_break (0); return GNUNET_SYSERR; } { size_t index; json_t *method; json_array_foreach (methods, index, method) { const char *t; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("type", &t), TALER_JSON_spec_amount_any ("usage_fee", cost), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (method, spec, NULL, NULL)) { GNUNET_break (0); continue; } if (0 == strcmp (t, type)) return GNUNET_OK; } } return GNUNET_NO; /* not found */ } /** * 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] rss 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 RecoverSecretState *rss, bool offline) { json_t *msg; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "No provider online, need user to manually specify providers!\n"); msg = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("hint", offline ? "could not contact provider" : "provider does not know you"), GNUNET_JSON_pack_bool ("offline", offline)); GNUNET_assert (0 == json_object_set_new (rss->state, "recovery_error", msg)); /* In case there are old ones, remove them! */ (void) json_object_del (rss->state, "recovery_document"); (void) json_object_del (rss->state, "recovery_information"); set_state (rss->state, ANASTASIS_RECOVERY_STATE_SECRET_SELECTING); rss->cb (rss->cb_cls, TALER_EC_NONE, rss->state); free_rss (rss); } /** * 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 * @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; struct RecoverSecretState *rss = pd->rss; json_t *policies; json_t *challenges; json_t *recovery_information; if (NULL == ri) { /* Woopsie, failed hard. */ GNUNET_CONTAINER_DLL_remove (rss->pd_head, rss->pd_tail, pd); ANASTASIS_recovery_abort (pd->recovery); GNUNET_free (pd->backend_url); GNUNET_free (pd); if (NULL != rss->pd_head) return; /* wait for another one */ /* all failed! report failure! */ return_no_policy (rss, 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; struct TALER_Amount cost; int ret; cd = ANASTASIS_challenge_get_details (c); ret = lookup_cost (rss->state, cd->provider_url, cd->type, &cost); if (GNUNET_SYSERR == ret) { json_decref (challenges); json_decref (policies); set_state (rss->state, ANASTASIS_RECOVERY_STATE_ERROR); ANASTASIS_redux_fail_ (rss->cb, rss->cb_cls, TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, "failed to 'lookup_cost'"); free_rss (rss); return; } cj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("uuid", &cd->uuid), TALER_JSON_pack_amount ("cost", (GNUNET_NO == ret) ? NULL : &cost), GNUNET_JSON_pack_string ("type", cd->type), 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 (rss->state, "recovery_information", recovery_information)); { json_t *rd; rd = ANASTASIS_recovery_serialize (pd->recovery); if (NULL == rd) { GNUNET_break (0); set_state (rss->state, ANASTASIS_RECOVERY_STATE_ERROR); rss->cb (rss->cb_cls, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, rss->state); free_rss (rss); return; } GNUNET_assert (0 == json_object_set_new (rss->state, "recovery_document", rd)); } /* In case there is an old error remove it! */ (void) json_object_del (rss->state, "recovery_error"); set_state (rss->state, ANASTASIS_RECOVERY_STATE_SECRET_SELECTING); rss->cb (rss->cb_cls, TALER_EC_NONE, rss->state); free_rss (rss); } /** * Try to launch recovery at provider @a provider_url with config @a p_cfg. * * @param[in,out] rss recovery context * @param provider_url base URL of the provider to try * @param p_cfg configuration of the provider * @return true if a recovery was launched */ static bool launch_recovery (struct RecoverSecretState *rss, const char *provider_url, const json_t *p_cfg) { struct PolicyDownloadEntry *pd = GNUNET_new (struct PolicyDownloadEntry); struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("salt", &pd->salt), GNUNET_JSON_spec_end () }; if (MHD_HTTP_OK != json_integer_value (json_object_get (p_cfg, "http_status"))) return false; /* skip providers that are down */ if (GNUNET_OK != GNUNET_JSON_parse (p_cfg, spec, NULL, NULL)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "No salt for `%s', provider offline?\n", provider_url); GNUNET_free (pd); return false; } pd->backend_url = GNUNET_strdup (provider_url); pd->rss = rss; pd->recovery = ANASTASIS_recovery_begin (ANASTASIS_REDUX_ctx_, rss->id_data, rss->have_version ? rss->version : 0, pd->backend_url, &pd->salt, &policy_lookup_cb, pd, &core_early_secret_cb, pd); if (NULL != pd->recovery) { GNUNET_CONTAINER_DLL_insert (rss->pd_head, rss->pd_tail, pd); return true; } GNUNET_free (pd->backend_url); GNUNET_free (pd); return false; } /** * We finished downloading /config from all providers, merge * into the main state, trigger the continuation and free our * state. * * @param[in] rss main state to merge into */ static void providers_complete (struct RecoverSecretState *rss) { bool launched = false; struct RecoveryStartStateProviderEntry *pe; json_t *tlist; tlist = json_object_get (rss->state, "authentication_providers"); if (NULL == tlist) { tlist = json_object (); GNUNET_assert (NULL != tlist); GNUNET_assert (0 == json_object_set_new (rss->state, "authentication_providers", tlist)); } while (NULL != (pe = rss->pe_head)) { json_t *provider_list; GNUNET_CONTAINER_DLL_remove (rss->pe_head, rss->pe_tail, pe); provider_list = json_object_get (pe->istate, "authentication_providers"); /* merge provider_list into tlist (overriding existing entries) */ if (NULL != provider_list) { const char *url; json_t *value; json_object_foreach (provider_list, url, value) { GNUNET_assert (0 == json_object_set (tlist, url, value)); } } json_decref (pe->istate); GNUNET_free (pe); } /* now iterate over providers and begin downloading */ if (NULL != rss->provider_url) { json_t *p_cfg; p_cfg = json_object_get (tlist, rss->provider_url); if (NULL != p_cfg) launched = launch_recovery (rss, rss->provider_url, p_cfg); } else { json_t *p_cfg; const char *provider_url; json_object_foreach (tlist, provider_url, p_cfg) { launched |= launch_recovery (rss, provider_url, p_cfg); } } if (! launched) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "No provider online, need user to specify different provider!\n"); return_no_policy (rss, true); return; } } /** * Function called when the complete information about a provider * was added to @a new_state. * * @param cls a `struct RecoveryStartStateProviderEntry` * @param error error code * @param new_state resulting new state */ static void provider_added_cb (void *cls, enum TALER_ErrorCode error, json_t *new_state) { struct RecoveryStartStateProviderEntry *pe = cls; pe->ra = NULL; pe->istate = json_incref (new_state); pe->ec = error; pe->rss->pending--; if (0 == pe->rss->pending) providers_complete (pe->rss); } /** * Start to query provider for recovery document. * * @param[in,out] rss overall recovery state * @param provider_url base URL of the provider to query */ static void begin_query_provider (struct RecoverSecretState *rss, const char *provider_url) { struct RecoveryStartStateProviderEntry *pe; json_t *istate; pe = GNUNET_new (struct RecoveryStartStateProviderEntry); pe->rss = rss; istate = json_object (); GNUNET_assert (NULL != istate); GNUNET_CONTAINER_DLL_insert (rss->pe_head, rss->pe_tail, pe); pe->ra = ANASTASIS_REDUX_add_provider_to_state_ (provider_url, istate, &provider_added_cb, pe); json_decref (istate); if (NULL != pe->ra) rss->pending++; } struct ANASTASIS_ReduxAction * ANASTASIS_REDUX_recovery_challenge_begin_ (json_t *state, const json_t *arguments, ANASTASIS_ActionCallback cb, void *cb_cls) { json_t *version; json_t *providers; const json_t *attributes; struct RecoverSecretState *rss; const char *provider_url; 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; } rss = GNUNET_new (struct RecoverSecretState); rss->id_data = json_incref ((json_t *) attributes); version = json_object_get (arguments, "version"); if (NULL != version) { rss->version = (unsigned int) json_integer_value (version); rss->have_version = true; } rss->state = json_incref (state); rss->cb = cb; rss->cb_cls = cb_cls; rss->pending = 1; /* decremented after initialization loop */ provider_url = json_string_value (json_object_get (arguments, "provider_url")); if (NULL != provider_url) { rss->provider_url = GNUNET_strdup (provider_url); begin_query_provider (rss, provider_url); } else { json_t *prov; const char *url; json_object_foreach (providers, url, prov) { begin_query_provider (rss, url); } } rss->pending--; if (0 == rss->pending) { providers_complete (rss); if (NULL == rss->cb) return NULL; } rss->ra.cleanup = &free_rss; rss->ra.cleanup_cls = rss; return &rss->ra; }