From 7e669bcf6b6336ec429da949bcb4aa456971dba2 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Fri, 30 Jul 2021 10:38:27 +0200 Subject: folding history in preparation of GNU Anastasis v0.0.0 release --- src/reducer/anastasis_api_recovery_redux.c | 2558 ++++++++++++++++++++++++++++ 1 file changed, 2558 insertions(+) create mode 100644 src/reducer/anastasis_api_recovery_redux.c (limited to 'src/reducer/anastasis_api_recovery_redux.c') diff --git a/src/reducer/anastasis_api_recovery_redux.c b/src/reducer/anastasis_api_recovery_redux.c new file mode 100644 index 0000000..8a900ec --- /dev/null +++ b/src/reducer/anastasis_api_recovery_redux.c @@ -0,0 +1,2558 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 2021 Taler Systems SA + + 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 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 lib/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; +}; + + +/** + * 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_GENERIC_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 ec 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; +} + + +/** + * 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_GENERIC_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 = json_pack ("{s:s}", + "state", + "solved"); + GNUNET_assert (NULL != 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; + json_t *val; + 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 = json_pack ( + "{s:s, s:s, s:I}", + "state", + "hint", + "hint", + s, + "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 = json_pack ( + "{s:s, s:o, s:I}", + "state", + "details", + "details", + body, + "http_status", + (json_int_t) csr->details.open_challenge.http_status); + } + } + else + { + /* unexpected / unsupported mime type */ + mime = NULL; + } + } + if (NULL == mime) + { + val = GNUNET_JSON_from_data (csr->details.open_challenge.body, + csr->details.open_challenge.body_size); + GNUNET_assert (NULL != val); + instructions = json_pack ( + "{s:s, s:o, s:I, s:s?}", + "state", + "body", + "body", + val, + "http_status", + (json_int_t) csr->details.open_challenge.http_status, + "mime_type", + mime); + } + GNUNET_assert (NULL != instructions); + 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 = json_pack ("{s:s, s:s}", + "state", + "redirect", + "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 = json_pack ("{s:s, s:s, s:s, s:o}", + "state", + "payment", + "taler_pay_uri", + csr->details.payment_required.taler_pay_uri, + "provider", + cd->provider_url, + "payment_secret", + GNUNET_JSON_from_data_auto ( + &csr->details.payment_required.payment_secret)); + GNUNET_assert (NULL != pay); + 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 = json_pack ("{s:s, s:I, s:I}", + "state", + "server-failure", + "http_status", + (json_int_t) csr->details.server_failure.http_status, + "error_code", + (json_int_t) csr->details.server_failure.ec); + GNUNET_assert (NULL != err); + 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_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_TRUTH_UNKNOWN: + { + json_t *err; + + err = json_pack ("{s:s, s:I}", + "state", + "truth-unknown", + "error_code", + (json_int_t) TALER_EC_ANASTASIS_TRUTH_UNKNOWN); + GNUNET_assert (NULL != err); + 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_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_RATE_LIMIT_EXCEEDED: + { + json_t *err; + + err = json_pack ("{s:s, s:I}", + "state", + "rate-limit-exceeded", + "error_code", + (json_int_t) TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED); + + GNUNET_assert (NULL != err); + 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_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 (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; + } + + /* 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; + } + + 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 */ + 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 (challenge, + "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); + + 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 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 */ + 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 = json_pack ("{s:I, s:O, s:s}", + "version", + (json_int_t) version, + "identity_attributes", + ia, + "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); + + +/** + * Operates on a recovery state depending on given #ANASTASIS_RecoveryState + * and #ANASTASIS_RecoveryAction. The new #ANASTASIS_RecoveryState is returned + * by a callback function. + * This function can do network access to talk to anastasis service providers. + * + * @param[in,out] state input/output state (to be modified) + * @param action what action to perform + * @param arguments data for the @a action + * @param cb function to call with the result + * @param cb_cls closure for @a cb + */ +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, + "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 #RecoverSecretState. + * + * @param cls closure for a #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 ec 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 = json_pack ("{s:s, s:b}", + "hint", + offline ? "could not contact provider" : + "provider does not know you", + "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 = json_pack ("{s:o}", + "uuid", + GNUNET_JSON_from_data_auto (&cd->uuid)); + GNUNET_assert (NULL != cj); + 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_GENERIC_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 = json_pack ("{s:o,s:o?,s:s,s:s}", + "uuid", + GNUNET_JSON_from_data_auto (&cd->uuid), + "cost", + (GNUNET_NO == ret) + ? NULL + : TALER_JSON_from_amount (&cost), + "type", + cd->type, + "instructions", + cd->instructions); + GNUNET_assert (NULL != cj); + GNUNET_assert (0 == + json_array_append_new (challenges, + cj)); + } /* end for all challenges */ + recovery_information = json_pack ("{s:o, s:o, s:s?, s:s, s:I}", + "challenges", challenges, + "policies", policies, + "secret_name", ri->secret_name, + "provider_url", pd->backend_url, + "version", (json_int_t) ri->version); + GNUNET_assert (NULL != recovery_information); + 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_GENERIC_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 (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; +} -- cgit v1.2.3