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_backup_redux.c | 4893 ++++++++++++++++++++++++++++++ 1 file changed, 4893 insertions(+) create mode 100644 src/reducer/anastasis_api_backup_redux.c (limited to 'src/reducer/anastasis_api_backup_redux.c') diff --git a/src/reducer/anastasis_api_backup_redux.c b/src/reducer/anastasis_api_backup_redux.c new file mode 100644 index 0000000..cea1360 --- /dev/null +++ b/src/reducer/anastasis_api_backup_redux.c @@ -0,0 +1,4893 @@ +/* + 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_backup_redux.c + * @brief anastasis reducer backup api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ + +#include "platform.h" +#include "anastasis_redux.h" +#include "anastasis_api_redux.h" +#include + +/** + * How long do Anastasis providers store data if the service + * is free? Must match #ANASTASIS_MAX_YEARS_STORAGE from + * anastasis-httpd.h. + */ +#define ANASTASIS_FREE_STORAGE GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_YEARS, 5) + +/** + * CPU limiter: do not evaluate more than 16k + * possible policy combinations to find the "best" + * policy. + */ +#define MAX_EVALUATIONS (1024 * 16) + + +#define GENERATE_STRING(STRING) #STRING, +static const char *backup_strings[] = { + ANASTASIS_BACKUP_STATES (GENERATE_STRING) +}; +#undef GENERATE_STRING + + +/** + * Linked list of costs. + */ +struct Costs +{ + + /** + * Kept in a LL. + */ + struct Costs *next; + + /** + * Cost in one of the currencies. + */ + struct TALER_Amount cost; +}; + + +/** + * Add amount from @a cost to @a my_cost list. + * + * @param[in,out] my_cost pointer to list to modify + * @param cost amount to add + */ +static void +add_cost (struct Costs **my_cost, + const struct TALER_Amount *cost) +{ + for (struct Costs *pos = *my_cost; + NULL != pos; + pos = pos->next) + { + if (GNUNET_OK != + TALER_amount_cmp_currency (&pos->cost, + cost)) + continue; + GNUNET_assert (0 <= + TALER_amount_add (&pos->cost, + &pos->cost, + cost)); + return; + } + { + struct Costs *nc; + + nc = GNUNET_new (struct Costs); + nc->cost = *cost; + nc->next = *my_cost; + *my_cost = nc; + } +} + + +enum ANASTASIS_BackupState +ANASTASIS_backup_state_from_string_ (const char *state_string) +{ + for (enum ANASTASIS_BackupState i = 0; + i < sizeof (backup_strings) / sizeof(*backup_strings); + i++) + if (0 == strcmp (state_string, + backup_strings[i])) + return i; + return ANASTASIS_BACKUP_STATE_ERROR; +} + + +const char * +ANASTASIS_backup_state_to_string_ (enum ANASTASIS_BackupState bs) +{ + if ( (bs < 0) || + (bs >= sizeof (backup_strings) / sizeof(*backup_strings)) ) + { + GNUNET_break_op (0); + return NULL; + } + return backup_strings[bs]; +} + + +/** + * Update the 'backup_state' field of @a state to @a new_backup_state. + * + * @param[in,out] state the state to transition + * @param new_backup_state the state to transition to + */ +static void +set_state (json_t *state, + enum ANASTASIS_BackupState new_backup_state) +{ + GNUNET_assert ( + 0 == + json_object_set_new ( + state, + "backup_state", + json_string (ANASTASIS_backup_state_to_string_ (new_backup_state)))); +} + + +/** + * Returns an initial ANASTASIS backup state (CONTINENT_SELECTING). + * + * @param cfg handle for gnunet configuration + * @return NULL on failure + */ +json_t * +ANASTASIS_backup_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_BACKUP_STATE_CONTINENT_SELECTING); + return initial_state; +} + + +/** + * Test if @a challenge_size is small enough for the provider's + * @a size_limit_in_mb. + * + * We add 1024 to @a challenge_size here as a "safety margin" as + * the encrypted challenge has some additional headers around it + * + * @param size_limit_in_mb provider's upload limit + * @param challenge_size actual binary size of the challenge + * @return true if this fits + */ +static bool +challenge_size_ok (uint32_t size_limit_in_mb, + size_t challenge_size) +{ + return (size_limit_in_mb * 1024LLU * 1024LLU >= + challenge_size + 1024LLU); +} + + +/** + * DispatchHandler/Callback function which is called for a + * "add_authentication" action. + * Returns an #ANASTASIS_ReduxAction if operation is async. + * + * @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 * +add_authentication (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *auth_providers; + json_t *method; + const char *method_type; + void *challenge; + size_t challenge_size; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &method_type), + GNUNET_JSON_spec_varsize ("challenge", + &challenge, + &challenge_size), + GNUNET_JSON_spec_end () + }; + + auth_providers = json_object_get (state, + "authentication_providers"); + if (NULL == auth_providers) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_providers' missing"); + return NULL; + } + + method = json_object_get (arguments, + "authentication_method"); + if (NULL == method) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'authentication_method' required"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (method, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_dumpf (method, + stderr, + JSON_INDENT (2)); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'authentication_method' content malformed"); + return NULL; + } + /* Check we know at least one provider that supports this method */ + { + bool found = false; + bool too_big = false; + json_t *details; + const char *url; + + json_object_foreach (auth_providers, url, details) + { + json_t *methods; + json_t *method; + size_t index; + uint32_t size_limit_in_mb; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", + &size_limit_in_mb), + GNUNET_JSON_spec_json ("methods", + &methods), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (details, + ispec, + NULL, NULL)) + { + GNUNET_break (0); + continue; + } + json_array_foreach (methods, index, method) + { + const char *type; + + type = json_string_value (json_object_get (method, + "type")); + GNUNET_break (NULL != type); + if ( (NULL != type) && + (0 == strcmp (type, + method_type)) ) + { + found = true; + break; + } + } + GNUNET_JSON_parse_free (ispec); + if (! challenge_size_ok (size_limit_in_mb, + challenge_size)) + { + /* Challenge data too big for this provider. Try to find another one. + Note: we add 1024 to challenge-size here as a "safety margin" as + the encrypted challenge has some additional headers around it */ + too_big = true; + found = false; + } + if (found) + break; + } + if (! found) + { + if (too_big) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_CHALLENGE_DATA_TOO_BIG, + method_type); + } + else + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_AUTHENTICATION_METHOD_NOT_SUPPORTED, + method_type); + } + GNUNET_JSON_parse_free (spec); + return NULL; + } + } + GNUNET_JSON_parse_free (spec); + + /* append provided method to our array */ + { + json_t *auth_method_arr; + + auth_method_arr = json_object_get (state, + "authentication_methods"); + if (NULL == auth_method_arr) + { + auth_method_arr = json_array (); + GNUNET_assert (0 == json_object_set_new (state, + "authentication_methods", + auth_method_arr)); + } + if (! json_is_array (auth_method_arr)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_methods' must be an array"); + return NULL; + } + GNUNET_assert (0 == + json_array_append (auth_method_arr, + method)); + cb (cb_cls, + TALER_EC_NONE, + state); + } + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "delete_authentication" action. + * Returns an #ANASTASIS_ReduxAction if operation is async. + * + * @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 * +del_authentication (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *idx; + json_t *auth_method_arr; + + auth_method_arr = json_object_get (state, + "authentication_methods"); + if (! json_is_array (auth_method_arr)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_methods' must be an array"); + return NULL; + } + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + idx = json_object_get (arguments, + "authentication_method"); + if ( (NULL == idx) || + (! json_is_integer (idx)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'authentication_method' must be a number"); + return NULL; + } + + { + size_t index = (size_t) json_integer_value (idx); + + if (0 != json_array_remove (auth_method_arr, + index)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "removal failed"); + return NULL; + } + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/* ********************** done_authentication ******************** */ + +/** + * Which provider would be used for the given challenge, + * and at what cost? + */ +struct PolicyEntry +{ + /** + * URL of the provider. + */ + const char *provider_name; + + /** + * Recovery fee. + */ + struct TALER_Amount usage_fee; +}; + + +/** + * Map from challenges to providers. + */ +struct PolicyMap +{ + /** + * Kept in a DLL. + */ + struct PolicyMap *next; + + /** + * Kept in a DLL. + */ + struct PolicyMap *prev; + + /** + * Array of proividers selected for each challenge, + * with associated costs. + * Length of the array will be 'req_methods'. + */ + struct PolicyEntry *providers; + + /** + * Diversity score for this policy mapping. + */ + unsigned int diversity; + +}; + + +/** + * Array of challenges for a policy, and DLL with + * possible mappings of challenges to providers. + */ +struct Policy +{ + + /** + * Kept in DLL of all possible policies. + */ + struct Policy *next; + + /** + * Kept in DLL of all possible policies. + */ + struct Policy *prev; + + /** + * Head of DLL. + */ + struct PolicyMap *pm_head; + + /** + * Tail of DLL. + */ + struct PolicyMap *pm_tail; + + /** + * Challenges selected for this policy. + * Length of the array will be 'req_methods'. + */ + unsigned int *challenges; + +}; + + +/** + * Information for running done_authentication() logic. + */ +struct PolicyBuilder +{ + /** + * Authentication providers available overall, from our state. + */ + json_t *providers; + + /** + * Authentication methods available overall, from our state. + */ + const json_t *methods; + + /** + * Head of DLL of all possible policies. + */ + struct Policy *p_head; + + /** + * Tail of DLL of all possible policies. + */ + struct Policy *p_tail; + + /** + * Array of authentication policies to be computed. + */ + json_t *policies; + + /** + * Array of length @e req_methods. + */ + unsigned int *m_idx; + + /** + * Array of length @e req_methods identifying a set of providers selected + * for each authentication method, while we are trying to compute the + * 'best' allocation of providers to authentication methods. + * Only valid during the go_with() function. + */ + const char **best_sel; + + /** + * Error hint to return on failure. Set if @e ec is not #TALER_EC_NONE. + */ + const char *hint; + + /** + * Policy we are currently building maps for. + */ + struct Policy *current_policy; + + /** + * LL of costs associated with the currently preferred + * policy. + */ + struct Costs *best_cost; + + /** + * Array of 'best' policy maps found so far, + * ordered by policy. + */ + struct PolicyMap *best_map; + + /** + * Array of the currency policy maps under evaluation + * by find_best_map(). + */ + struct PolicyMap *curr_map; + + /** + * How many mappings have we evaluated so far? + * Used to limit the computation by aborting after + * #MAX_EVALUATIONS trials. + */ + unsigned int evaluations; + + /** + * Overall number of challenges provided by the user. + */ + unsigned int num_methods; + + /** + * Number of challenges that must be satisfied to recover the secret. + * Derived from the total number of challenges entered by the user. + */ + unsigned int req_methods; + + /** + * Number of different Anastasis providers selected in @e best_sel. + * Only valid during the go_with() function. + */ + unsigned int best_diversity; + + /** + * Number of identical challenges duplicated at + * various providers in the best case. Smaller is + * better. + */ + unsigned int best_duplicates; + + /** + * Error code to return, #TALER_EC_NONE on success. + */ + enum TALER_ErrorCode ec; + +}; + + +/** + * Free @a costs LL. + * + * @param[in] costs linked list to free + */ +static void +free_costs (struct Costs *costs) +{ + if (NULL == costs) + return; + free_costs (costs->next); + GNUNET_free (costs); +} + + +/** + * Check if providers @a p1 and @a p2 have equivalent + * methods and cost structures. + * + * @return true if the providers are fully equivalent + */ +static bool +equiv_provider (struct PolicyBuilder *pb, + const char *p1, + const char *p2) +{ + json_t *j1; + json_t *j2; + json_t *m1; + json_t *m2; + struct TALER_Amount uc1; + struct TALER_Amount uc2; + + j1 = json_object_get (pb->providers, + p1); + j2 = json_object_get (pb->providers, + p2); + if ( (NULL == j1) || + (NULL == j2) ) + { + GNUNET_break (0); + return false; + } + + { + struct GNUNET_JSON_Specification s1[] = { + GNUNET_JSON_spec_json ("methods", + &m1), + TALER_JSON_spec_amount_any ("truth_upload_fee", + &uc1), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j1, + s1, + NULL, NULL)) + { + GNUNET_break (0); + return false; + } + } + + { + struct GNUNET_JSON_Specification s2[] = { + GNUNET_JSON_spec_json ("methods", + &m2), + TALER_JSON_spec_amount_any ("truth_upload_fee", + &uc2), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j2, + s2, + NULL, NULL)) + { + GNUNET_break (0); + return false; + } + } + + if ( (GNUNET_OK != + TALER_amount_cmp_currency (&uc1, + &uc2)) || + (0 != + TALER_amount_cmp (&uc1, + &uc2)) ) + return false; + + if (json_array_size (m1) != json_array_size (m2)) + return false; + { + size_t idx1; + json_t *e1; + + json_array_foreach (m1, idx1, e1) + { + const char *type1; + struct TALER_Amount fee1; + struct GNUNET_JSON_Specification s1[] = { + GNUNET_JSON_spec_string ("type", + &type1), + TALER_JSON_spec_amount_any ("usage_fee", + &fee1), + GNUNET_JSON_spec_end () + }; + bool matched = false; + + if (GNUNET_OK != + GNUNET_JSON_parse (e1, + s1, + NULL, NULL)) + { + GNUNET_break (0); + return false; + } + { + size_t idx2; + json_t *e2; + + json_array_foreach (m2, idx2, e2) + { + const char *type2; + struct TALER_Amount fee2; + struct GNUNET_JSON_Specification s2[] = { + GNUNET_JSON_spec_string ("type", + &type2), + TALER_JSON_spec_amount_any ("usage_fee", + &fee2), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (e2, + s2, + NULL, NULL)) + { + GNUNET_break (0); + return false; + } + if ( (0 == strcmp (type1, + type2)) && + (GNUNET_OK == + TALER_amount_cmp_currency (&fee1, + &fee2)) && + (0 == TALER_amount_cmp (&fee1, + &fee2)) ) + { + matched = true; + break; + } + } + } + if (! matched) + return false; + } + } + return true; +} + + +/** + * Evaluate the cost/benefit of the provider selection in @a prov_sel + * and if it is better then the best known one in @a pb, update @a pb. + * + * @param[in,out] pb our operational context + * @param[in,out] prov_sel array of req_methods provider indices to complete + */ +static void +eval_provider_selection (struct PolicyBuilder *pb, + const char *prov_sel[]) +{ + unsigned int curr_diversity; + struct PolicyEntry policy_ent[pb->req_methods]; + + for (unsigned int i = 0; i < pb->req_methods; i++) + { + const json_t *method_obj = json_array_get (pb->methods, + pb->m_idx[i]); + const json_t *provider_cfg = json_object_get (pb->providers, + prov_sel[i]); + json_t *provider_methods; + const char *method_type; + json_t *md; + size_t index; + bool found = false; + uint32_t size_limit_in_mb; + struct TALER_Amount upload_cost; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", + &size_limit_in_mb), + GNUNET_JSON_spec_json ("methods", + &provider_methods), + TALER_JSON_spec_amount_any ("truth_upload_fee", + &upload_cost), + GNUNET_JSON_spec_end () + }; + void *challenge; + size_t challenge_size; + struct GNUNET_JSON_Specification mspec[] = { + GNUNET_JSON_spec_string ("type", + &method_type), + GNUNET_JSON_spec_varsize ("challenge", + &challenge, + &challenge_size), + GNUNET_JSON_spec_end () + }; + + policy_ent[i].provider_name = prov_sel[i]; + if (GNUNET_OK != + GNUNET_JSON_parse (method_obj, + mspec, + NULL, NULL)) + { + GNUNET_break (0); + pb->ec = TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID; + pb->hint = "'authentication_method' content malformed"; + return; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (provider_cfg, + pspec, + NULL, NULL)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Skipping provider %s: no suitable configuration\n", + prov_sel[i]); + GNUNET_JSON_parse_free (mspec); + return; + } + json_array_foreach (provider_methods, index, md) + { + const char *type; + struct TALER_Amount method_cost; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + TALER_JSON_spec_amount_any ("usage_fee", + &method_cost), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (md, + spec, + NULL, NULL)) + { + GNUNET_break (0); + pb->ec = TALER_EC_ANASTASIS_REDUCER_STATE_INVALID; + pb->hint = "'methods' of provider"; + GNUNET_JSON_parse_free (pspec); + return; + } + if ( (0 == strcmp (type, + method_type)) && + (challenge_size_ok (size_limit_in_mb, + challenge_size) ) ) + { + found = true; + GNUNET_break (0 <= + TALER_amount_add (&policy_ent[i].usage_fee, + &method_cost, + &upload_cost)); + } + } + if (! found) + { + /* Provider does not OFFER this method, combination not possible. + Cost is basically 'infinite', but we simply then skip this. */ + GNUNET_JSON_parse_free (pspec); + GNUNET_JSON_parse_free (mspec); + return; + } + GNUNET_JSON_parse_free (mspec); + GNUNET_JSON_parse_free (pspec); + } + + /* calculate provider diversity by counting number of different + providers selected */ + curr_diversity = 0; + for (unsigned int i = 0; i < pb->req_methods; i++) + { + bool found = false; + + for (unsigned int j = 0; j < i; j++) + { + if (prov_sel[i] == prov_sel[j]) + { + found = true; + break; + } + } + if (! found) + curr_diversity++; + } +#if DEBUG + fprintf (stderr, + "Diversity: %u (best: %u)\n", + curr_diversity, + pb->best_diversity); +#endif + if (curr_diversity < pb->best_diversity) + return; /* do not allow combinations that are bad + for provider diversity */ + if (curr_diversity > pb->best_diversity) + { + /* drop existing policies, they are all worse */ + struct PolicyMap *m; + + while (NULL != (m = pb->current_policy->pm_head)) + { + GNUNET_CONTAINER_DLL_remove (pb->current_policy->pm_head, + pb->current_policy->pm_tail, + m); + GNUNET_free (m->providers); + GNUNET_free (m); + } + pb->best_diversity = curr_diversity; + } + if (NULL == pb->p_head) + { + /* For the first policy, check for equivalent + policy mapping existing: we + do not want to do spend CPU time investigating + purely equivalent permutations */ + for (struct PolicyMap *m = pb->current_policy->pm_head; + NULL != m; + m = m->next) + { + bool equiv = true; + for (unsigned int i = 0; ireq_methods; i++) + { + if (! equiv_provider (pb, + m->providers[i].provider_name, + policy_ent[i].provider_name)) + { + equiv = false; + break; + } + } + if (equiv) + return; /* equivalent to known allocation */ + } + } + + /* Add possible mapping to result list */ + { + struct PolicyMap *m; + + m = GNUNET_new (struct PolicyMap); + m->providers = GNUNET_new_array (pb->req_methods, + struct PolicyEntry); + memcpy (m->providers, + policy_ent, + sizeof (struct PolicyEntry) * pb->req_methods); + m->diversity = curr_diversity; + GNUNET_CONTAINER_DLL_insert (pb->current_policy->pm_head, + pb->current_policy->pm_tail, + m); + } +} + + +/** + * Recursively compute possible combination(s) of provider candidates + * in @e prov_sel. The selection is complete up to index @a i. Calls + * eval_provider_selection() upon a feasible provider selection for + * evaluation, resulting in "better" combinations being persisted in + * @a pb. + * + * @param[in,out] pb our operational context + * @param[in,out] prov_sel array of req_methods provider URLs to complete + * @param i index up to which @a prov_sel is already initialized + */ +static void +provider_candidate (struct PolicyBuilder *pb, + const char *prov_sel[], + unsigned int i) +{ + const char *url; + json_t *pconfig; + + json_object_foreach (pb->providers, url, pconfig) + { + prov_sel[i] = url; + if (i == pb->req_methods - 1) + { + eval_provider_selection (pb, + prov_sel); + if (TALER_EC_NONE != pb->ec) + break; + continue; + } + provider_candidate (pb, + prov_sel, + i + 1); + } +} + + +/** + * Using the selection of authentication methods from @a pb in + * "m_idx", compute the best choice of providers. + * + * @param[in,out] pb our operational context + */ +static void +go_with (struct PolicyBuilder *pb) +{ + const char *prov_sel[pb->req_methods]; + struct Policy *policy; + + /* compute provider selection */ + policy = GNUNET_new (struct Policy); + policy->challenges = GNUNET_new_array (pb->req_methods, + unsigned int); + memcpy (policy->challenges, + pb->m_idx, + pb->req_methods * sizeof (unsigned int)); + pb->current_policy = policy; + pb->best_diversity = 0; + provider_candidate (pb, + prov_sel, + 0); + GNUNET_CONTAINER_DLL_insert (pb->p_head, + pb->p_tail, + policy); + pb->current_policy = NULL; +} + + +/** + * Recursively computes all possible subsets of length "req_methods" + * from an array of length "num_methods", calling "go_with" on each of + * those subsets (in "m_idx"). + * + * @param[in,out] pb our operational context + * @param i offset up to which the "m_idx" has been computed + */ +static void +method_candidate (struct PolicyBuilder *pb, + unsigned int i) +{ + unsigned int start; + unsigned int *m_idx = pb->m_idx; + + start = (i > 0) ? m_idx[i - 1] + 1 : 0; + for (unsigned int j = start; j < pb->num_methods; j++) + { + m_idx[i] = j; + if (i == pb->req_methods - 1) + { +#if DEBUG + fprintf (stderr, + "Suggesting: "); + for (unsigned int k = 0; kreq_methods; k++) + { + fprintf (stderr, + "%u ", + m_idx[k]); + } + fprintf (stderr, "\n"); +#endif + go_with (pb); + continue; + } + method_candidate (pb, + i + 1); + } +} + + +/** + * Lookup @a salt of @a provider_url in @a state. + * + * @param state the state to inspect + * @param provider_url provider to look into + * @param[out] salt value to extract + * @return #GNUNET_OK on success + */ +static int +lookup_salt (const json_t *state, + const char *provider_url, + struct ANASTASIS_CRYPTO_ProviderSaltP *salt) +{ + const json_t *aps; + const json_t *cfg; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("salt", + salt), + GNUNET_JSON_spec_end () + }; + + aps = json_object_get (state, + "authentication_providers"); + if (NULL == aps) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + cfg = json_object_get (aps, + provider_url); + if (NULL == cfg) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_JSON_parse (cfg, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Compare two cost lists. + * + * @param my cost to compare + * @param be cost to compare + * @return 0 if costs are estimated equal, + * 1 if @a my < @a be + * -1 if @a my > @a be + */ +static int +compare_costs (const struct Costs *my, + const struct Costs *be) +{ + int ranking = 0; + + for (const struct Costs *cmp = be; + NULL != cmp; + cmp = cmp->next) + { + bool found = false; + + for (const struct Costs *pos = my; + NULL != pos; + pos = pos->next) + { + if (GNUNET_OK != + TALER_amount_cmp_currency (&cmp->cost, + &pos->cost)) + continue; + found = true; + } + if (! found) + ranking--; /* new policy has no cost in this currency */ + } + + for (const struct Costs *pos = my; + NULL != pos; + pos = pos->next) + { + bool found = false; + + for (const struct Costs *cmp = be; + NULL != cmp; + cmp = cmp->next) + { + if (GNUNET_OK != + TALER_amount_cmp_currency (&cmp->cost, + &pos->cost)) + continue; + found = true; + switch (TALER_amount_cmp (&cmp->cost, + &pos->cost)) + { + case -1: /* cmp < pos */ + ranking--; + break; + case 0: + break; + case 1: /* cmp > pos */ + ranking++; + break; + } + break; + } + if (! found) + ranking++; /* old policy has no cost in this currency */ + } + if (0 == ranking) + return 0; + return (0 > ranking) ? -1 : 1; +} + + +/** + * Evaluate the combined policy map stack in the ``curr_map`` of @a pb + * and compare to the current best cost. If we are better, save the + * stack in the ``best_map``. + * + * @param[in,out] pb policy builder we evaluate for + * @param num_policies length of the ``curr_map`` array + */ +static void +evaluate_map (struct PolicyBuilder *pb, + unsigned int num_policies) +{ + struct Costs *my_cost = NULL; + unsigned int i = 0; + unsigned int duplicates = 0; + int ccmp; + +#if DEBUG + fprintf (stderr, + "Checking...\n"); +#endif + /* calculate cost */ + for (const struct Policy *p = pb->p_head; + NULL != p; + p = p->next) + { + const struct PolicyMap *pm = &pb->curr_map[i++]; + +#if DEBUG + fprintf (stderr, + "Evaluating %p (%u): ", + p, + pm->diversity); + for (unsigned int k = 0; kreq_methods; k++) + { + const struct PolicyEntry *pe = &pm->providers[k]; + + fprintf (stderr, + "%u->%s ", + p->challenges[k], + pe->provider_name); + } + fprintf (stderr, "\n"); +#endif + for (unsigned int j = 0; jreq_methods; j++) + { + const struct PolicyEntry *pe = &pm->providers[j]; + unsigned int cv = p->challenges[j]; + bool found = false; + unsigned int i2 = 0; + + /* check for duplicates */ + for (const struct Policy *p2 = pb->p_head; + p2 != p; + p2 = p2->next) + { + const struct PolicyMap *pm2 = &pb->curr_map[i2++]; + + for (unsigned int j2 = 0; j2req_methods; j2++) + { + const struct PolicyEntry *pe2 = &pm2->providers[j2]; + unsigned int cv2 = p2->challenges[j2]; + + if (cv != cv2) + continue; /* different challenge */ + if (0 == strcmp (pe->provider_name, + pe2->provider_name)) + found = true; /* same challenge&provider! */ + else + duplicates++; /* penalty for same challenge at two providers */ + } + } + if (found) + continue; /* cost already included, do not add */ + add_cost (&my_cost, + &pe->usage_fee); + } + } + + ccmp = -1; /* non-zero if 'best_duplicates' is UINT_MAX */ + if ( (UINT_MAX != pb->best_duplicates) && + (0 > (ccmp = compare_costs (my_cost, + pb->best_cost))) ) + { + /* new method not clearly better, do not use it */ + free_costs (my_cost); +#if DEBUG + fprintf (stderr, + "... useless\n"); +#endif + return; + } + if ( (0 == ccmp) && + (duplicates > pb->best_duplicates) ) + { + /* new method is cost-equal, but looses on duplicates, + do not use it */ + free_costs (my_cost); +#if DEBUG + fprintf (stderr, + "... useless\n"); +#endif + return; + } + /* new method is better (or first), set as best */ +#if DEBUG + fprintf (stderr, + "New best: %u duplicates, %s cost\n", + duplicates, + TALER_amount2s (&my_cost->cost)); +#endif + free_costs (pb->best_cost); + pb->best_cost = my_cost; + pb->best_duplicates = duplicates; + memcpy (pb->best_map, + pb->curr_map, + sizeof (struct PolicyMap) * num_policies); +} + + +/** + * Try all policy maps for @a pos and evaluate the + * resulting total cost, saving the best result in + * @a pb. + * + * @param[in,out] pb policy builder context + * @param pos policy we are currently looking at maps for + * @param off index of @a pos for the policy map + */ +static void +find_best_map (struct PolicyBuilder *pb, + struct Policy *pos, + unsigned int off) +{ + if (NULL == pos) + { + evaluate_map (pb, + off); + pb->evaluations++; + return; + } + for (struct PolicyMap *pm = pos->pm_head; + NULL != pm; + pm = pm->next) + { + pb->curr_map[off] = *pm; + find_best_map (pb, + pos->next, + off + 1); + if (pb->evaluations >= MAX_EVALUATIONS) + break; + } +} + + +/** + * Select cheapest policy combinations and add them to the JSON ``policies`` + * array in @a pb + * + * @param[in,out] policy builder with our state + */ +static void +select_policies (struct PolicyBuilder *pb) +{ + unsigned int cnt = 0; + + for (struct Policy *p = pb->p_head; + NULL != p; + p = p->next) + cnt++; + { + struct PolicyMap best[cnt]; + struct PolicyMap curr[cnt]; + unsigned int i; + + pb->best_map = best; + pb->curr_map = curr; + pb->best_duplicates = UINT_MAX; /* worst */ + find_best_map (pb, + pb->p_head, + 0); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Assessed %u/%u policies\n", + pb->evaluations, + (unsigned int) MAX_EVALUATIONS); + i = 0; + for (struct Policy *p = pb->p_head; + NULL != p; + p = p->next) + { + struct PolicyMap *pm = &best[i++]; + json_t *method_arr; + +#if DEBUG + fprintf (stderr, + "Best map (%u): ", + pm->diversity); + for (unsigned int k = 0; kreq_methods; k++) + { + fprintf (stderr, + "%u->%s ", + p->challenges[k], + pm->providers[k].provider_name); + } + fprintf (stderr, "\n"); +#endif + /* Convert "best" selection into 'policies' array */ + method_arr = json_array (); + GNUNET_assert (NULL != method_arr); + for (unsigned int i = 0; i < pb->req_methods; i++) + { + json_t *policy_method = json_pack ("{s:I, s:s}", + "authentication_method", + (json_int_t) p->challenges[i], + "provider", + pm->providers[i].provider_name); + GNUNET_assert (0 == + json_array_append_new (method_arr, + policy_method)); + } + { + json_t *policy = json_pack ("{s:o}", + "methods", + method_arr); + GNUNET_assert (NULL != policy); + GNUNET_assert (0 == + json_array_append_new (pb->policies, + policy)); + } + } + } +} + + +/** + * Clean up @a pb, in particular the policies DLL. + * + * @param[in] pb builder to clean up + */ +static void +clean_pb (struct PolicyBuilder *pb) +{ + struct Policy *p; + + while (NULL != (p = pb->p_head)) + { + struct PolicyMap *pm; + + while (NULL != (pm = p->pm_head)) + { + GNUNET_CONTAINER_DLL_remove (p->pm_head, + p->pm_tail, + pm); + GNUNET_free (pm->providers); + GNUNET_free (pm); + } + GNUNET_CONTAINER_DLL_remove (pb->p_head, + pb->p_tail, + p); + GNUNET_free (p->challenges); + GNUNET_free (p); + } +} + + +/** + * DispatchHandler/Callback function which is called for a + * "done_authentication" action. Automaticially computes policies + * based on available Anastasis providers and challenges provided by + * the user. + * + * @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_authentication (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct PolicyBuilder pb = { + .ec = TALER_EC_NONE + }; + json_t *providers; + json_t *policy_providers; + + pb.providers = json_object_get (state, + "authentication_providers"); + if ( (NULL == pb.providers) || + (! json_is_object (pb.providers) ) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_providers' must be provided"); + return NULL; + } + pb.methods = json_object_get (state, + "authentication_methods"); + if ( (NULL == pb.methods) || + (! json_is_array (pb.methods)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_methods' must be provided"); + return NULL; + } + pb.num_methods = json_array_size (pb.methods); + switch (pb.num_methods) + { + case 0: + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_methods' must not be empty"); + return NULL; + case 1: + case 2: + pb.req_methods = pb.num_methods; + break; + case 3: + case 4: + pb.req_methods = pb.num_methods - 1; + break; + case 5: + case 6: + pb.req_methods = pb.num_methods - 2; + break; + case 7: + pb.req_methods = pb.num_methods - 3; + break; + default: + /* cap at 4 for auto-generation, algorithm + to compute mapping gets too expensive + otherwise. */ + pb.req_methods = 4; + break; + } + { + unsigned int m_idx[pb.req_methods]; + + /* select req_methods from num_methods. */ + pb.m_idx = m_idx; + method_candidate (&pb, + 0); + } + pb.policies = json_array (); + select_policies (&pb); + clean_pb (&pb); + if (TALER_EC_NONE != pb.ec) + { + json_decref (pb.policies); + ANASTASIS_redux_fail_ (cb, + cb_cls, + pb.ec, + pb.hint); + return NULL; + } + GNUNET_assert (0 == + json_object_set_new (state, + "policies", + pb.policies)); + providers = json_object_get (arguments, + "providers"); + if (NULL == providers) + { + /* Setup a providers array from all working providers */ + json_t *available = json_object_get (state, + "authentication_providers"); + const char *url; + json_t *details; + + policy_providers = json_array (); + json_object_foreach (available, url, details) + { + json_t *provider; + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + + if (GNUNET_OK != + lookup_salt (state, + url, + &salt)) + continue; /* skip providers that are down */ + provider = json_pack ("{s:s}", + "provider_url", url); + GNUNET_assert (NULL != provider); + GNUNET_assert (0 == + json_array_append_new (policy_providers, + provider)); + } + } + else + { + /* Setup a providers array from all working providers */ + size_t off; + json_t *url; + + policy_providers = json_array (); + json_array_foreach (providers, off, url) + { + json_t *provider; + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + const char *url_str; + + url_str = json_string_value (url); + if ( (NULL == url_str) || + (GNUNET_OK != + lookup_salt (state, + url_str, + &salt)) ) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "unworkable provider requested"); + return NULL; + } + provider = json_pack ("{s:s}", + "provider_url", url); + GNUNET_assert (NULL != provider); + GNUNET_assert (0 == + json_array_append_new (policy_providers, + provider)); + } + } + if (0 == json_array_size (policy_providers)) + { + json_decref (policy_providers); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "no workable providers in state"); + return NULL; + } + GNUNET_assert (0 == + json_object_set_new (state, + "policy_providers", + policy_providers)); + set_state (state, + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/* ******************** add_provider ******************* */ + + +/** + * DispatchHandler/Callback function which is called for a + * "add_provider" action. Adds another Anastasis provider + * to the list of available providers for storing information. + * + * @param state state to operate on + * @param arguments arguments with a provider URL to add + * @param cb callback to call during/after operation + * @param cb_cls callback closure + */ +static struct ANASTASIS_ReduxAction * +add_provider (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + if (ANASTASIS_add_provider_ (state, + arguments, + cb, + cb_cls)) + return NULL; + return ANASTASIS_REDUX_backup_begin_ (state, + NULL, + cb, + cb_cls); +} + + +/* ******************** add_policy ******************* */ + + +/** + * DispatchHandler/Callback function which is called for a + * "add_policy" action. + * + * @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 * +add_policy (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *arg_array; + json_t *policies; + const json_t *auth_providers; + const json_t *auth_methods; + json_t *methods; + + if (NULL == arguments) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + arg_array = json_object_get (arguments, + "policy"); + if (! json_is_array (arg_array)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'policy' not an array"); + return NULL; + } + policies = json_object_get (state, + "policies"); + if (! json_is_array (policies)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' not an array"); + return NULL; + } + auth_providers = json_object_get (state, + "authentication_providers"); + if (! json_is_object (auth_providers)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'auth_providers' not an object"); + return NULL; + } + auth_methods = json_object_get (state, + "authentication_methods"); + if (! json_is_array (auth_methods)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'auth_methods' not an array"); + return NULL; + } + + methods = json_array (); + GNUNET_assert (NULL != methods); + + /* Add all methods from 'arg_array' to 'methods' */ + { + size_t index; + json_t *method; + + json_array_foreach (arg_array, index, method) + { + const char *provider_url; + uint32_t method_idx; + json_t *prov_methods; + const char *method_type; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("provider", + &provider_url), + GNUNET_JSON_spec_uint32 ("authentication_method", + &method_idx), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (method, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'method' details malformed"); + return NULL; + } + + { + const json_t *prov_cfg; + uint32_t limit; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", + &limit), + GNUNET_JSON_spec_json ("methods", + &prov_methods), + GNUNET_JSON_spec_end () + }; + + prov_cfg = json_object_get (auth_providers, + provider_url); + if (NULL == prov_cfg) + { + GNUNET_break (0); + json_decref (methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "provider URL unknown"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (prov_cfg, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "provider lacks authentication methods"); + return NULL; + + } + if (! json_is_array (prov_methods)) + { + GNUNET_break (0); + json_decref (methods); + json_decref (prov_methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "provider lacks authentication methods"); + return NULL; + } + } + + { + const json_t *auth_method; + + auth_method = json_array_get (auth_methods, + method_idx); + if (NULL == auth_method) + { + GNUNET_break (0); + json_decref (methods); + json_decref (prov_methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "authentication method unknown"); + return NULL; + } + method_type = json_string_value (json_object_get (auth_method, + "type")); + if (NULL == method_type) + { + GNUNET_break (0); + json_decref (methods); + json_decref (prov_methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "authentication method must be a string"); + return NULL; + } + } + + { + bool found = false; + size_t index; + json_t *pm; + json_array_foreach (prov_methods, index, pm) + { + struct TALER_Amount method_cost; + const char *type; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + TALER_JSON_spec_amount_any ("usage_fee", + &method_cost), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (pm, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (methods); + json_decref (prov_methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "provider authentication method specification invalid"); + return NULL; + } + if (0 != strcmp (type, + method_type)) + continue; + found = true; + break; + } + if (! found) + { + GNUNET_break (0); + json_decref (methods); + json_decref (prov_methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "selected provider does not support authentication method"); + return NULL; + } + } + GNUNET_assert (0 == + json_array_append (methods, + method)); + json_decref (prov_methods); + } /* end of json_array_foreach (arg_array, index, method) */ + } + + /* add new policy to array of existing policies */ + { + json_t *policy; + json_t *idx; + + policy = json_pack ("{s:o}", + "methods", + methods); + GNUNET_assert (NULL != policy); + idx = json_object_get (arguments, + "policy_index"); + if ( (NULL == idx) || + (! json_is_integer (idx)) ) + { + GNUNET_assert (0 == + json_array_append_new (policies, + policy)); + } + else + { + GNUNET_assert (0 == + json_array_insert_new (policies, + json_integer_value (idx), + policy)); + } + } + + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/* ******************** update_policy ******************* */ + + +/** + * DispatchHandler/Callback function which is called for a + * "update_policy" action. + * + * @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 * +update_policy (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *idx; + size_t index; + json_t *policy_arr; + + if (NULL == arguments) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + idx = json_object_get (arguments, + "policy_index"); + if (! json_is_integer (idx)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'policy_index' must be an integer"); + return NULL; + } + index = json_integer_value (idx); + policy_arr = json_object_get (state, + "policies"); + if (! json_is_array (policy_arr)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' must be an array"); + return NULL; + } + if (0 != json_array_remove (policy_arr, + index)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "removal failed"); + return NULL; + } + return add_policy (state, + arguments, + cb, + cb_cls); +} + + +/* ******************** del_policy ******************* */ + + +/** + * DispatchHandler/Callback function which is called for a + * "delete_policy" action. + * + * @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 * +del_policy (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *idx; + size_t index; + json_t *policy_arr; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + idx = json_object_get (arguments, + "policy_index"); + if (! json_is_integer (idx)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'policy_index' must be an integer"); + return NULL; + } + index = json_integer_value (idx); + policy_arr = json_object_get (state, + "policies"); + if (! json_is_array (policy_arr)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' must be an array"); + return NULL; + } + if (0 != json_array_remove (policy_arr, + index)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "removal failed"); + return NULL; + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/* ******************** del_challenge ******************* */ + + +/** + * DispatchHandler/Callback function which is called for a + * "delete_challenge" action. + * + * @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 * +del_challenge (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *pidx; + const json_t *cidx; + size_t index; + json_t *policy_arr; + json_t *policy; + json_t *method_arr; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + pidx = json_object_get (arguments, + "policy_index"); + cidx = json_object_get (arguments, + "challenge_index"); + if (! json_is_integer (pidx)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'policy_index' must be an integer"); + return NULL; + } + if (! json_is_integer (cidx)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'challenge_index' must be an integer"); + return NULL; + } + index = json_integer_value (pidx); + policy_arr = json_object_get (state, + "policies"); + if (! json_is_array (policy_arr)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' must be an array"); + return NULL; + } + policy = json_array_get (policy_arr, + index); + if (NULL == policy) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'policy_index' out of range"); + return NULL; + } + method_arr = json_object_get (policy, + "methods"); + if (NULL == method_arr) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "methods missing in policy"); + return NULL; + } + index = json_integer_value (cidx); + if (0 != json_array_remove (method_arr, + index)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "removal failed"); + return NULL; + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/* ********************** done_policy_review ***************** */ + + +/** + * Calculate how many years of service we need + * from the desired @a expiration time, + * rounding up. + * + * @param expiration desired expiration time + * @return number of years of service to pay for +*/ +static unsigned int +expiration_to_years (struct GNUNET_TIME_Absolute expiration) +{ + struct GNUNET_TIME_Relative rem; + unsigned int years; + + rem = GNUNET_TIME_absolute_get_remaining (expiration); + years = rem.rel_value_us / GNUNET_TIME_UNIT_YEARS.rel_value_us; + if (0 != rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us) + years++; + return years; +} + + +/** + * Update @a state such that the earliest expiration for + * any truth or policy is @a expiration. Recalculate + * the ``upload_fees`` array with the associated costs. + * + * @param[in,out] state our state to update + * @param expiration new expiration to enforce + * @return #GNUNET_OK on success, + * #GNUNET_SYSERR if the state is invalid + */ +static enum GNUNET_GenericReturnValue +update_expiration_cost (json_t *state, + struct GNUNET_TIME_Absolute expiration) +{ + struct Costs *costs = NULL; + unsigned int years; + json_t *providers; + bool is_free = true; + + providers = json_object_get (state, + "authentication_providers"); + if (! json_is_object (providers)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + years = expiration_to_years (expiration); + + /* go over all providers and add up cost */ + { + const char *url; + json_t *provider; + + json_object_foreach (providers, url, provider) + { + struct TALER_Amount annual_fee; + struct GNUNET_JSON_Specification pspec[] = { + TALER_JSON_spec_amount_any ("annual_fee", + &annual_fee), + GNUNET_JSON_spec_end () + }; + struct TALER_Amount fee; + + if (GNUNET_OK != + GNUNET_JSON_parse (provider, + pspec, + NULL, NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_multiply (&fee, + &annual_fee, + years)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + add_cost (&costs, + &fee); + } + } + + /* go over all truths and add up cost */ + { + unsigned int off = 0; + unsigned int len = 0; + struct AlreadySeen + { + uint32_t method; + const char *provider_url; + } *seen = NULL; + json_t *policies; + size_t pidx; + json_t *policy; + + policies = json_object_get (state, + "policies"); + json_array_foreach (policies, pidx, policy) + { + json_t *methods; + json_t *method; + size_t midx; + + methods = json_object_get (policy, + "methods"); + json_array_foreach (methods, midx, method) + { + const char *provider_url; + uint32_t method_idx; + + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("provider", + &provider_url), + GNUNET_JSON_spec_uint32 ("authentication_method", + &method_idx), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (method, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + /* check if we have seen this one before */ + { + bool found = false; + + for (unsigned int i = 0; i + TALER_amount_multiply (&fee, + &upload_cost, + years)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + add_cost (&costs, + &fee); + } + } + } + GNUNET_array_grow (seen, + len, + 0); + } + + /* convert 'costs' into state */ + { + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + while (NULL != costs) + { + struct Costs *nxt = costs->next; + + if ( (0 != costs->cost.value) || + (0 != costs->cost.fraction) ) + { + json_t *ao; + + ao = json_pack ("{s:o}", + "fee", + TALER_JSON_from_amount (&costs->cost)); + GNUNET_assert (0 == + json_array_append_new (arr, + ao)); + is_free = false; + } + GNUNET_free (costs); + costs = nxt; + } + GNUNET_assert (0 == + json_object_set_new (state, + "upload_fees", + arr)); + } + + if (is_free) + expiration = GNUNET_TIME_relative_to_absolute (ANASTASIS_FREE_STORAGE); + /* update 'expiration' in state */ + { + json_t *eo; + + (void) GNUNET_TIME_round_abs (&expiration); + eo = GNUNET_JSON_from_time_abs (expiration); + GNUNET_assert (0 == + json_object_set_new (state, + "expiration", + eo)); + } + + + return GNUNET_OK; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "done_policy_review" action. + * + * @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_policy_review (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *policy_arr; + + policy_arr = json_object_get (state, + "policies"); + if (0 == json_array_size (policy_arr)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "no policies specified"); + return NULL; + } + { + struct GNUNET_TIME_Absolute exp = {0}; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_absolute_time ("expiration", + &exp)), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "invalid expiration specified"); + return NULL; + } + if (0 == exp.abs_value_us) + exp = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_YEARS); + if (GNUNET_OK != + update_expiration_cost (state, + exp)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "could not calculate expiration cost"); + return NULL; + } + } + set_state (state, + ANASTASIS_BACKUP_STATE_SECRET_EDITING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * Information we keep for an upload() operation. + */ +struct UploadContext; + + +/** + * Maps a TruthUpload to a policy and recovery method where this + * truth is used. + */ +struct PolicyMethodReference +{ + /** + * Offset into the "policies" array. + */ + unsigned int policy_index; + + /** + * Offset into the "methods" array (of the policy selected + * by @e policy_index). + */ + unsigned int method_index; + +}; + + +/** + * Entry we keep per truth upload. + */ +struct TruthUpload +{ + + /** + * Kept in a DLL. + */ + struct TruthUpload *next; + + /** + * Kept in a DLL. + */ + struct TruthUpload *prev; + + /** + * Handle to the actual upload operation. + */ + struct ANASTASIS_TruthUpload *tu; + + /** + * Upload context this operation is part of. + */ + struct UploadContext *uc; + + /** + * Truth resulting from the upload, if any. + */ + struct ANASTASIS_Truth *t; + + /** + * A taler://pay/-URI with a request to pay the annual fee for + * the service. Set if payment is required. + */ + char *payment_request; + + /** + * Which policies and methods does this truth affect? + */ + struct PolicyMethodReference *policies; + + /** + * Where are we uploading to? + */ + char *provider_url; + + /** + * Which challenge object are we uploading? + */ + uint32_t am_idx; + + /** + * Length of the @e policies array. + */ + unsigned int policies_length; + + /** + * Status of the upload. + */ + enum ANASTASIS_UploadStatus us; + + /** + * Taler error code of the upload. + */ + enum TALER_ErrorCode ec; + +}; + + +/** + * Information we keep for an upload() operation. + */ +struct UploadContext +{ + /** + * Recovery action returned to caller for aborting the operation. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * Function to call upon completion. + */ + ANASTASIS_ActionCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Our state. + */ + json_t *state; + + /** + * Master secret sharing operation, NULL if not yet running. + */ + struct ANASTASIS_SecretShare *ss; + + /** + * Head of DLL of truth uploads. + */ + struct TruthUpload *tues_head; + + /** + * Tail of DLL of truth uploads. + */ + struct TruthUpload *tues_tail; + + /** + * Timeout to use for the operation, from the arguments. + */ + struct GNUNET_TIME_Relative timeout; + + /** + * For how many years should we pay? + */ + unsigned int years; + +}; + + +/** + * Function called when the #upload transition is being aborted. + * + * @param cls a `struct UploadContext` + */ +static void +upload_cancel_cb (void *cls) +{ + struct UploadContext *uc = cls; + struct TruthUpload *tue; + + while (NULL != (tue = uc->tues_head)) + { + GNUNET_CONTAINER_DLL_remove (uc->tues_head, + uc->tues_tail, + tue); + if (NULL != tue->tu) + { + ANASTASIS_truth_upload_cancel (tue->tu); + tue->tu = NULL; + } + if (NULL != tue->t) + { + ANASTASIS_truth_free (tue->t); + tue->t = NULL; + } + GNUNET_free (tue->provider_url); + GNUNET_free (tue->payment_request); + GNUNET_free (tue->policies); + GNUNET_free (tue); + } + if (NULL != uc->ss) + { + ANASTASIS_secret_share_cancel (uc->ss); + uc->ss = NULL; + } + json_decref (uc->state); + GNUNET_free (uc); +} + + +/** + * Take all of the ongoing truth uploads and serialize them into the @a uc + * state. + * + * @param[in,out] uc context to take truth uploads from and to update state of + */ +static void +serialize_truth (struct UploadContext *uc) +{ + json_t *policies; + + policies = json_object_get (uc->state, + "policies"); + GNUNET_assert (json_is_array (policies)); + for (struct TruthUpload *tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + if (NULL == tue->t) + continue; + for (unsigned int i = 0; ipolicies_length; i++) + { + const struct PolicyMethodReference *pmr = &tue->policies[i]; + json_t *policy = json_array_get (policies, + pmr->policy_index); + json_t *methods = json_object_get (policy, + "methods"); + json_t *auth_method = json_array_get (methods, + pmr->method_index); + json_t *truth = ANASTASIS_truth_to_json (tue->t); + + GNUNET_assert (0 == + json_object_set_new (truth, + "upload_status", + json_integer (tue->us))); + GNUNET_assert (NULL != policy); + GNUNET_assert (NULL != methods); + GNUNET_assert (NULL != auth_method); + GNUNET_assert (NULL != truth); + GNUNET_assert (0 == + json_object_set_new (auth_method, + "truth", + truth)); + } + } +} + + +/** + * Function called with the results of a #ANASTASIS_secret_share(). + * + * @param cls closure with a `struct UploadContext *` + * @param sr share result + */ +static void +secret_share_result_cb (void *cls, + const struct ANASTASIS_ShareResult *sr) +{ + struct UploadContext *uc = cls; + + uc->ss = NULL; + switch (sr->ss) + { + case ANASTASIS_SHARE_STATUS_SUCCESS: + /* Just to be safe, delete the "core_secret" so that it is not + accidentally preserved anywhere */ + (void) json_object_del (uc->state, + "core_secret"); + { + json_t *sa = json_object (); + + GNUNET_assert (NULL != sa); + for (unsigned int i = 0; idetails.success.num_providers; i++) + { + const struct ANASTASIS_ProviderSuccessStatus *pssi + = &sr->details.success.pss[i]; + json_t *d; + + d = json_pack ("{s:I, s:o}", + "policy_version", + pssi->policy_version, + "policy_expiration", + GNUNET_JSON_from_time_abs (pssi->policy_expiration)); + GNUNET_assert (NULL != d); + GNUNET_assert (0 == + json_object_set_new (sa, + pssi->provider_url, + d)); + } + GNUNET_assert (0 == + json_object_set_new (uc->state, + "success_details", + sa)); + } + set_state (uc->state, + ANASTASIS_BACKUP_STATE_BACKUP_FINISHED); + uc->cb (uc->cb_cls, + TALER_EC_NONE, + uc->state); + break; + case ANASTASIS_SHARE_STATUS_PAYMENT_REQUIRED: + { + json_t *ra; + json_t *providers; + + providers = json_object_get (uc->state, + "policy_providers"); + set_state (uc->state, + ANASTASIS_BACKUP_STATE_POLICIES_PAYING); + serialize_truth (uc); + ra = json_array (); + GNUNET_assert (NULL != ra); + for (unsigned int i = 0; i< + sr->details.payment_required.payment_requests_length; i++) + { + const struct ANASTASIS_SharePaymentRequest *spr; + json_t *pr; + size_t off; + json_t *provider; + + spr = &sr->details.payment_required.payment_requests[i]; + pr = json_pack ("{s:s, s:s}", + "payto", + spr->payment_request_url, + "provider", + spr->provider_url); + GNUNET_assert (0 == + json_array_append_new (ra, + pr)); + json_array_foreach (providers, off, provider) + { + const char *purl = json_string_value (json_object_get (provider, + "provider_url")); + + if (NULL == purl) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "policy_providers array contents are invalid"); + json_decref (ra); + return; + } + if (0 == strcmp (purl, + spr->provider_url)) + { + json_t *psj; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Remembering payment secret for provider `%s'\n", + spr->provider_url); + psj = GNUNET_JSON_from_data_auto (&spr->payment_secret); + GNUNET_assert (0 == + json_object_set_new (provider, + "payment_secret", + psj)); + } + } + } + GNUNET_assert (0 == + json_object_set_new (uc->state, + "policy_payment_requests", + ra)); + } + uc->cb (uc->cb_cls, + TALER_EC_NONE, + uc->state); + break; + case ANASTASIS_SHARE_STATUS_PROVIDER_FAILED: + { + json_t *details; + + details = json_pack ("{s:s, s:I, s:I, s:s}", + "backup_state", + "ERROR", + "http_status", + (json_int_t) sr->details.provider_failure.http_status, + "upload_status", + (json_int_t) sr->details.provider_failure.ec, + "provider_url", + sr->details.provider_failure.provider_url); + uc->cb (uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED, + details); + json_decref (details); + } + break; + default: + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected share result"); + break; + } + upload_cancel_cb (uc); +} + + +/** + * All truth uploads are done, begin with uploading the policy. + * + * @param[in,out] uc context for the operation + */ +static void +share_secret (struct UploadContext *uc) +{ + json_t *user_id; + json_t *core_secret; + json_t *jpolicies; + json_t *providers = NULL; + size_t policies_len; + const char *secret_name = NULL; + unsigned int pds_len; + struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("identity_attributes", + &user_id), + GNUNET_JSON_spec_json ("policies", + &jpolicies), + GNUNET_JSON_spec_json ("policy_providers", + &providers), + GNUNET_JSON_spec_json ("core_secret", + &core_secret), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("secret_name", + &secret_name)), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (uc->state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "State parsing failed when preparing to share secret"); + upload_cancel_cb (uc); + return; + } + + { + json_t *args; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &timeout)), + GNUNET_JSON_spec_end () + }; + + args = json_object_get (uc->state, + "pay-arguments"); + if ( (NULL != args) && + (GNUNET_OK != + GNUNET_JSON_parse (args, + pspec, + NULL, NULL)) ) + { + json_dumpf (args, + stderr, + JSON_INDENT (2)); + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + NULL); + upload_cancel_cb (uc); + return; + } + } + + if ( (! json_is_object (user_id)) || + (! json_is_array (jpolicies)) || + (0 == json_array_size (jpolicies)) || + ( (NULL != providers) && + (! json_is_array (providers)) ) ) + { + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "State parsing failed checks when preparing to share secret"); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + + policies_len = json_array_size (jpolicies); + pds_len = json_array_size (providers); + + if (0 == pds_len) + { + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "no workable providers in state"); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + + + { + struct ANASTASIS_Policy *vpolicies[policies_len]; + const struct ANASTASIS_Policy *policies[policies_len]; + struct ANASTASIS_ProviderDetails pds[GNUNET_NZL (pds_len)]; + + /* initialize policies/vpolicies arrays */ + memset (pds, + 0, + sizeof (pds)); + for (size_t i = 0; icb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'methods' must be an array"); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + methods_len = json_array_size (jmethods); + { + struct ANASTASIS_Policy *p; + struct ANASTASIS_Truth *truths[methods_len]; + const struct ANASTASIS_Truth *ctruths[methods_len]; + + for (unsigned int j = 0; jcb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'truth' failed to decode"); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + if (NULL != jtruth) + { + /* Get truth by deserializing from state */ + truths[j] = ANASTASIS_truth_from_json (jtruth); + if (NULL == truths[j]) + { + GNUNET_break (0); + for (unsigned int k = 0; kcb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'truth' failed to decode"); + GNUNET_JSON_parse_free (ispec); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + } + else + { + bool found = false; + /* Maybe we never serialized the truth; find it in our DLL */ + for (struct TruthUpload *tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + GNUNET_break (NULL != tue->t); + if ( (tue->am_idx == truth_index) && + (0 == strcmp (provider_url, + tue->provider_url)) ) + { + /* Duplicate truth object */ + json_t *jt = ANASTASIS_truth_to_json (tue->t); + + GNUNET_assert (NULL != jt); + truths[j] = ANASTASIS_truth_from_json (jt); + GNUNET_assert (NULL != truths[j]); + json_decref (jt); + found = true; + break; + } + } + if (! found) + { + GNUNET_break (0); + for (unsigned int k = 0; kcb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'truth' failed to decode"); + GNUNET_JSON_parse_free (ispec); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + } + GNUNET_JSON_parse_free (ispec); + ctruths[j] = truths[j]; + } + p = ANASTASIS_policy_create (ctruths, + methods_len); + vpolicies[i] = p; + policies[i] = p; + for (unsigned int k = 0; kstate, + pds[i].provider_url, + &pds[i].provider_salt)) ) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'providers' entry malformed"); + for (unsigned int i = 0; iss = ANASTASIS_secret_share (ANASTASIS_REDUX_ctx_, + user_id, + pds, + pds_len, + policies, + policies_len, + uc->years, + timeout, + &secret_share_result_cb, + uc, + secret_name, + secret, + secret_size); + GNUNET_free (secret); + } + for (unsigned int i = 0; iss) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Failed to begin secret sharing"); + upload_cancel_cb (uc); + return; + } +} + + +/** + * Some truth uploads require payment, serialize state and + * request payment to be executed by the application. + * + * @param[in,out] uc context for the operation + */ +static void +request_truth_payment (struct UploadContext *uc) +{ + json_t *payments; + + payments = json_array (); + GNUNET_assert (NULL != payments); + serialize_truth (uc); + for (struct TruthUpload *tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + if (NULL == tue->payment_request) + continue; + GNUNET_assert ( + 0 == + json_array_append_new (payments, + json_string ( + tue->payment_request))); + } + GNUNET_assert (0 == + json_object_set_new (uc->state, + "payments", + payments)); + set_state (uc->state, + ANASTASIS_BACKUP_STATE_TRUTHS_PAYING); + uc->cb (uc->cb_cls, + TALER_EC_NONE, + uc->state); + upload_cancel_cb (uc); +} + + +/** + * We may be finished with all (active) asynchronous operations. + * Check if any are pending and continue accordingly. + * + * @param[in,out] uc context for the operation + */ +static void +check_upload_finished (struct UploadContext *uc) +{ + bool pay = false; + bool active = false; + + for (struct TruthUpload *tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + if (TALER_EC_NONE != tue->ec) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Truth upload failed with error %d\n", + (int) tue->ec); + uc->cb (uc->cb_cls, + tue->ec, + NULL); + upload_cancel_cb (uc); + return; + } + if (NULL != tue->tu) + active = true; + if (NULL != tue->payment_request) + pay = true; + } + if (active) + return; + if (pay) + { + request_truth_payment (uc); + return; + } + share_secret (uc); +} + + +/** + * Upload result information. The resulting truth object can be used + * to create policies. If payment is required, the @a taler_pay_url + * is returned and the operation must be retried after payment. + * Callee MUST free @a t using ANASTASIS_truth_free(). + * + * @param cls closure with a `struct TruthUpload` + * @param t truth object to create policies, NULL on failure + * @param ud upload details + */ +static void +truth_upload_cb (void *cls, + struct ANASTASIS_Truth *t, + const struct ANASTASIS_UploadDetails *ud) +{ + struct TruthUpload *tue = cls; + + tue->tu = NULL; + tue->t = t; + tue->ec = ud->ec; + tue->us = ud->us; + if (ANASTASIS_US_PAYMENT_REQUIRED == ud->us) + { + tue->payment_request = GNUNET_strdup ( + ud->details.payment.payment_request); + } + check_upload_finished (tue->uc); +} + + +/** + * Check if we still need to create a new truth object for the truth + * identified by @a provider_url and @a am_idx. If so, create it from + * @a truth for policy reference @a pmr. If such a truth object + * already exists, append @a pmr to its list of reasons. + * + * @param[in,out] our upload context + * @param pmr policy method combination that requires the truth + * @param provider_url the URL of the Anastasis provider to upload + * the truth to, used to check for existing entries + * @param am_idx index of the authentication method, used to check for existing entries + * @param[in] truth object representing already uploaded truth, reference captured! + * @param[in,out] async_truth pointer to counter with the number of ongoing uploads, + * updated + * @param auth_method object with the challenge details, to generate the truth + * @return #GNUNET_SYSERR error requiring abort, + * #GNUNET_OK on success + */ +static int +add_truth_object (struct UploadContext *uc, + const struct PolicyMethodReference *pmr, + const char *provider_url, + uint32_t am_idx, + json_t *truth, + unsigned int *async_truth, + json_t *auth_method) +{ + /* check if we are already uploading this truth */ + struct TruthUpload *tue; + bool must_upload = true; + + for (tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + if ( (0 == strcmp (tue->provider_url, + provider_url)) && + (am_idx == tue->am_idx) ) + { + GNUNET_array_append (tue->policies, + tue->policies_length, + *pmr); + break; + } + } + + if (NULL == tue) + { + /* Create new entry */ + tue = GNUNET_new (struct TruthUpload); + + GNUNET_CONTAINER_DLL_insert (uc->tues_head, + uc->tues_tail, + tue); + tue->uc = uc; + tue->policies = GNUNET_new (struct PolicyMethodReference); + *tue->policies = *pmr; + tue->provider_url = GNUNET_strdup (provider_url); + tue->am_idx = am_idx; + tue->policies_length = 1; + } + + { + uint32_t status = UINT32_MAX; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("upload_status", + &status)), + GNUNET_JSON_spec_end () + }; + if (GNUNET_OK != + GNUNET_JSON_parse (truth, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + must_upload = (ANASTASIS_US_SUCCESS != status); + } + + if (NULL == tue->t) + { + tue->t = ANASTASIS_truth_from_json (truth); + if (NULL == tue->t) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + } + + if ( (NULL != tue->tu) && + (! must_upload) ) + { + ANASTASIS_truth_upload_cancel (tue->tu); + (*async_truth)--; + tue->tu = NULL; + return GNUNET_OK; + } + + if ( (NULL == tue->tu) && + (must_upload) ) + { + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + struct ANASTASIS_CRYPTO_UserIdentifierP id; + void *truth_data; + size_t truth_data_size; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_varsize ("challenge", + &truth_data, + &truth_data_size), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + lookup_salt (uc->state, + provider_url, + &salt)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_JSON_parse (auth_method, + spec, + NULL, NULL)) + { + json_dumpf (auth_method, + stderr, + JSON_INDENT (2)); + GNUNET_break (0); + return GNUNET_SYSERR; + } + { + json_t *user_id; + + user_id = json_object_get (uc->state, + "identity_attributes"); + if (! json_is_object (user_id)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + ANASTASIS_CRYPTO_user_identifier_derive (user_id, + &salt, + &id); + } + tue->tu = ANASTASIS_truth_upload3 (ANASTASIS_REDUX_ctx_, + &id, + tue->t, + truth_data, + truth_data_size, + uc->years, + uc->timeout, + &truth_upload_cb, + tue); + GNUNET_JSON_parse_free (spec); + tue->t = NULL; + (*async_truth)++; + } + + if ( (NULL != tue->tu) && + (NULL != tue->t) ) + { + /* no point in having both */ + ANASTASIS_truth_free (tue->t); + tue->t = NULL; + } + return GNUNET_OK; +} + + +/** + * Check if we still need to upload the truth identified by + * @a provider_url and @a am_idx. If so, upload it for + * policy reference @a pmr. If the upload is already queued, + * append @a pmr to its list of reasons. + * + * @param[in,out] our upload context + * @param pmr policy method combination that requires the truth + * @param provider_url the URL of the Anastasis provider to upload + * the truth to, used to check for existing entries + * @param am_idx index of the authentication method, used to check for existing entries + * @param auth_method object with the challenge details, to generate the truth + * @return #GNUNET_SYSERR on error requiring abort, + * #GNUNET_NO if no new truth upload was generated (@a pmr was appended) + * #GNUNET_OK if a new truth upload was initiated + */ +static int +check_truth_upload (struct UploadContext *uc, + const struct PolicyMethodReference *pmr, + const char *provider_url, + uint32_t am_idx, + json_t *auth_method) +{ + json_t *user_id; + json_t *jtruth; + struct TruthUpload *tue; + + user_id = json_object_get (uc->state, + "identity_attributes"); + if (! json_is_object (user_id)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* check if we are already uploading this truth */ + for (tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + if ( (0 == strcmp (tue->provider_url, + provider_url)) && + (am_idx == tue->am_idx) ) + { + GNUNET_array_append (tue->policies, + tue->policies_length, + *pmr); + return GNUNET_NO; + } + } + + /* need new upload */ + tue = GNUNET_new (struct TruthUpload); + { + json_t *policies = json_object_get (uc->state, + "policies"); + json_t *policy = json_array_get (policies, + pmr->policy_index); + json_t *methods = json_object_get (policy, + "methods"); + json_t *method = json_array_get (methods, + pmr->method_index); + + jtruth = json_object_get (method, + "truth"); + } + + { + const char *type; + const char *mime_type = NULL; + const char *instructions = NULL; + void *truth_data; + size_t truth_data_size; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("mime_type", + &mime_type)), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("instructions", + &instructions)), + GNUNET_JSON_spec_varsize ("challenge", + &truth_data, + &truth_data_size), + GNUNET_JSON_spec_end () + }; + struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt; + struct ANASTASIS_CRYPTO_UserIdentifierP id; + + if (GNUNET_OK != + GNUNET_JSON_parse (auth_method, + spec, + NULL, NULL)) + { + json_dumpf (auth_method, + stderr, + JSON_INDENT (2)); + GNUNET_break (0); + GNUNET_free (tue); + return GNUNET_SYSERR; + } + GNUNET_CONTAINER_DLL_insert (uc->tues_head, + uc->tues_tail, + tue); + tue->uc = uc; + tue->policies = GNUNET_new (struct PolicyMethodReference); + *tue->policies = *pmr; + tue->provider_url = GNUNET_strdup (provider_url); + tue->am_idx = am_idx; + tue->policies_length = 1; + if (GNUNET_OK != + lookup_salt (uc->state, + provider_url, + &provider_salt)) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return GNUNET_SYSERR; + } + ANASTASIS_CRYPTO_user_identifier_derive (user_id, + &provider_salt, + &id); + { + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + struct ANASTASIS_CRYPTO_QuestionSaltP question_salt; + struct ANASTASIS_CRYPTO_TruthKeyP truth_key; + struct ANASTASIS_CRYPTO_KeyShareP key_share; + struct ANASTASIS_CRYPTO_NonceP nonce; + + struct GNUNET_JSON_Specification jspec[] = { + GNUNET_JSON_spec_fixed_auto ("salt", + &question_salt), + GNUNET_JSON_spec_fixed_auto ("truth_key", + &truth_key), + GNUNET_JSON_spec_fixed_auto ("nonce", + &nonce), + GNUNET_JSON_spec_fixed_auto ("uuid", + &uuid), + GNUNET_JSON_spec_fixed_auto ("key_share", + &key_share), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (jtruth, + jspec, + NULL, NULL)) + { + tue->tu = ANASTASIS_truth_upload (ANASTASIS_REDUX_ctx_, + &id, + provider_url, + type, + instructions, + mime_type, + &provider_salt, + truth_data, + truth_data_size, + uc->years, + uc->timeout, + &truth_upload_cb, + tue); + } + else + { + tue->tu = ANASTASIS_truth_upload2 (ANASTASIS_REDUX_ctx_, + &id, + provider_url, + type, + instructions, + mime_type, + &provider_salt, + truth_data, + truth_data_size, + uc->years, + uc->timeout, + &nonce, + &uuid, + &question_salt, + &truth_key, + &key_share, + &truth_upload_cb, + tue); + } + } + if (NULL == tue->tu) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return GNUNET_SYSERR; + } + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; + } +} + + +/** + * Function to upload truths and recovery document policies. + * Ultimately transitions to failed state (allowing user to go back + * and change providers/policies), or payment, or finished. + * + * @param state state to operate on + * @param truth_indices indices of truths to upload explicitly + * @param cb callback (#ANASTASIS_ActionCallback) to call after upload + * @param cb_cls callback closure + */ +static struct ANASTASIS_ReduxAction * +upload (json_t *state, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct UploadContext *uc; + json_t *auth_methods; + json_t *policies; + struct GNUNET_TIME_Absolute expiration; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_absolute_time ("expiration", + &expiration), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'expiration' missing"); + return NULL; + } + auth_methods = json_object_get (state, + "authentication_methods"); + if ( (! json_is_array (auth_methods)) || + (0 == json_array_size (auth_methods)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_methods' must be non-empty array"); + return NULL; + } + policies = json_object_get (state, + "policies"); + if ( (! json_is_array (policies)) || + (0 == json_array_size (policies)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' must be non-empty array"); + return NULL; + } + + uc = GNUNET_new (struct UploadContext); + uc->ra.cleanup = &upload_cancel_cb; + uc->ra.cleanup_cls = uc; + uc->cb = cb; + uc->cb_cls = cb_cls; + uc->state = json_incref (state); + uc->years = expiration_to_years (expiration); + + { + json_t *args; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &uc->timeout)), + GNUNET_JSON_spec_end () + }; + + args = json_object_get (uc->state, + "pay-arguments"); + if ( (NULL != args) && + (GNUNET_OK != + GNUNET_JSON_parse (args, + pspec, + NULL, NULL)) ) + { + json_dumpf (args, + stderr, + JSON_INDENT (2)); + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'timeout' must be valid delay"); + + return NULL; + } + } + + { + json_t *policy; + size_t pindex; + unsigned int async_truth = 0; + + json_array_foreach (policies, pindex, policy) + { + json_t *methods = json_object_get (policy, + "methods"); + json_t *auth_method; + size_t mindex; + + if ( (! json_is_array (methods)) || + (0 == json_array_size (policies)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' must be non-empty array"); + upload_cancel_cb (uc); + return NULL; + } + json_array_foreach (methods, mindex, auth_method) + { + uint32_t am_idx; + const char *provider_url; + json_t *truth = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("provider", + &provider_url), + GNUNET_JSON_spec_uint32 ("authentication_method", + &am_idx), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("truth", + &truth)), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (auth_method, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'method' data malformed"); + upload_cancel_cb (uc); + return NULL; + } + { + struct PolicyMethodReference pmr = { + .policy_index = pindex, + .method_index = mindex + }; + json_t *amj; + + amj = json_array_get (auth_methods, + am_idx); + if (NULL == amj) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_method' refers to invalid authorization index malformed"); + upload_cancel_cb (uc); + GNUNET_JSON_parse_free (spec); + return NULL; + } + if (NULL == truth) + { + int ret; + + ret = check_truth_upload (uc, + &pmr, + provider_url, + am_idx, + amj); + if (GNUNET_SYSERR == ret) + { + GNUNET_JSON_parse_free (spec); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + NULL); + return NULL; + } + if (GNUNET_OK == ret) + async_truth++; + } + else + { + int ret; + + ret = add_truth_object (uc, + &pmr, + provider_url, + am_idx, + truth, + &async_truth, + amj); + if (GNUNET_SYSERR == ret) + { + GNUNET_JSON_parse_free (spec); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + NULL); + return NULL; + } + } + } + GNUNET_JSON_parse_free (spec); + } /* end for all methods of policy */ + } /* end for all policies */ + if (async_truth > 0) + return &uc->ra; + } + share_secret (uc); + if (NULL == uc->ss) + return NULL; + return &uc->ra; +} + + +/** + * Test if the core secret @a secret_size is small enough to be stored + * at all providers, which have a minimum upload limit of @a min_limit_in_mb. + * + * For now, we do not precisely calculate the size of the recovery document, + * and simply assume that the instructions (i.e. security questions) are all + * relatively small (aka sane), and that the number of authentication methods + * and recovery policies is similarly small so that all of this meta data + * fits in 512 kb (which is VERY big). + * + * Even with the minimum permitted upload limit of 1 MB (which is likely, + * given that there is hardly a reason for providers to offer more), this + * leaves 512 kb for the @a secret_size, which should be plenty (given + * that this is supposed to be for a master key, and not the actual data). + * + * @param state our state, could be used in the future to calculate the + * size of the recovery document without the core secret + * @param secret_size size of the core secret + * @param min_limit_in_mb minimum upload size of all providers + */ +static bool +core_secret_fits (const json_t *state, + size_t secret_size, + uint32_t min_limit_in_mb) +{ + return (min_limit_in_mb * 1024LL * 1024LL > + 512LLU * 1024LLU + secret_size); +} + + +/** + * Check if the upload size limit is satisfied. + * + * @param state our state + * @param jsecret the uploaded secret + * @return #GNUNET_OK if @a secret_size works for all providers, + * #GNUNET_NO if the @a secret_size is too big, + * #GNUNET_SYSERR if a provider has a limit of 0 + */ +static enum GNUNET_GenericReturnValue +check_upload_size_limit (json_t *state, + const json_t *jsecret) +{ + uint32_t min_limit = UINT32_MAX; + json_t *aps = json_object_get (state, + "authentication_providers"); + const char *url; + json_t *ap; + size_t secret_size; + + { + char *secret; + + secret = json_dumps (jsecret, + JSON_COMPACT | JSON_SORT_KEYS); + GNUNET_assert (NULL != secret); + secret_size = strlen (secret); + GNUNET_free (secret); + } + + /* We calculate the minimum upload limit of all possible providers; + this is under the (simplified) assumption that we store the + recovery document at all providers; this may be changed later, + see #6760. */ + json_object_foreach (aps, url, ap) + { + uint32_t limit; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", + &limit), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (ap, + spec, + NULL, NULL)) + { + /* skip malformed provider, likely /config failed */ + continue; + } + if (0 == limit) + return GNUNET_SYSERR; + min_limit = GNUNET_MIN (min_limit, + limit); + } + if (! core_secret_fits (state, + secret_size, + min_limit)) + return GNUNET_NO; + return GNUNET_OK; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "enter_secret" action. + * + * @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 * +enter_secret (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *jsecret; + struct GNUNET_TIME_Absolute expiration = {0}; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("secret", + &jsecret), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_absolute_time ("expiration", + &expiration)), + 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, + "'secret' argument required"); + return NULL; + } + + /* check upload size limit */ + { + enum GNUNET_GenericReturnValue ret; + + ret = check_upload_size_limit (state, + jsecret); + switch (ret) + { + case GNUNET_SYSERR: + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "provider has an upload limit of 0"); + return NULL; + case GNUNET_NO: + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_SECRET_TOO_BIG, + NULL); + return NULL; + default: + break; + } + } + if (0 != expiration.abs_value_us) + { + if (GNUNET_OK != + update_expiration_cost (state, + expiration)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "could not calculate expiration cost"); + return NULL; + } + } + GNUNET_assert (0 == + json_object_set (state, + "core_secret", + jsecret)); + cb (cb_cls, + TALER_EC_NONE, + state); + GNUNET_JSON_parse_free (spec); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "clear_secret" action. + * + * @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 * +clear_secret (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + if (0 != + json_object_del (state, + "core_secret")) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'core_secret' not set"); + return NULL; + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for an + * "enter_secret_name" action. + * + * @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 * +enter_secret_name (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const char *secret_name = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &secret_name), + 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, + "'name' argument required"); + return NULL; + } + + GNUNET_assert (0 == + json_object_set_new (state, + "secret_name", + json_string (secret_name))); + cb (cb_cls, + TALER_EC_NONE, + state); + GNUNET_JSON_parse_free (spec); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for the + * "update_expiration" action in the "secret editing" state. + * Updates how long we are to store the truth and policies + * and computes the new cost. + * + * @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 (synchronous operation) + */ +static struct ANASTASIS_ReduxAction * +update_expiration (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct GNUNET_TIME_Absolute expiration; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_absolute_time ("expiration", + &expiration), + GNUNET_JSON_spec_end () + }; + + if (NULL == arguments) + { + GNUNET_break (0); + 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)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'expiration' argument required"); + return NULL; + } + if (GNUNET_OK != + update_expiration_cost (state, + expiration)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "could not calculate expiration cost"); + return NULL; + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for the + * "next" action in the "secret editing" state. + * Returns an #ANASTASIS_ReduxAction as operation is async. + * + * @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 + */ +static struct ANASTASIS_ReduxAction * +finish_secret (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *core_secret; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("core_secret", + &core_secret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "State parsing failed: 'core_secret' is missing"); + return NULL; + } + + /* check upload size limit */ + { + enum GNUNET_GenericReturnValue ret; + + ret = check_upload_size_limit (state, + core_secret); + switch (ret) + { + case GNUNET_SYSERR: + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "provider has an upload limit of 0"); + GNUNET_JSON_parse_free (spec); + return NULL; + case GNUNET_NO: + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_SECRET_TOO_BIG, + NULL); + GNUNET_JSON_parse_free (spec); + return NULL; + default: + break; + } + } + + GNUNET_JSON_parse_free (spec); + return upload (state, + cb, + cb_cls); +} + + +/** + * DispatchHandler/Callback function which is called for a + * "pay" action. + * Returns an #ANASTASIS_ReduxAction as operation is async. + * + * @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 + */ +static struct ANASTASIS_ReduxAction * +pay_truths_backup (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + /* Clear 'payments' if it exists */ + (void) json_object_del (state, + "payments"); + if (NULL != arguments) + GNUNET_assert (0 == + json_object_set (state, + "pay-arguments", + (json_t *) arguments)); + return upload (state, + cb, + cb_cls); +} + + +/** + * DispatchHandler/Callback function which is called for a + * "pay" action. + * Returns an #ANASTASIS_ReduxAction as operation is async. + * + * @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 + */ +static struct ANASTASIS_ReduxAction * +pay_policies_backup (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + /* Clear 'policy_payment_requests' if it exists */ + (void) json_object_del (state, + "policy_payment_requests"); + if (NULL != arguments) + GNUNET_assert (0 == + json_object_set (state, + "pay-arguments", + (json_t *) arguments)); + return upload (state, + cb, + cb_cls); +} + + +/** + * DispatchHandler/Callback function which is called for a + * "back" action if state is "FINISHED". + * + * @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 * +back_finished (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + set_state (state, + ANASTASIS_BACKUP_STATE_SECRET_EDITING); + 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 backup state depending on given #ANASTASIS_BackupState + * and #ANASTASIS_BackupAction. The new #ANASTASIS_BackupState 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_backup_action_ (json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct Dispatcher + { + enum ANASTASIS_BackupState backup_state; + const char *backup_action; + DispatchHandler fun; + } dispatchers[] = { + { + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING, + "add_authentication", + &add_authentication + }, + { + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING, + "delete_authentication", + &del_authentication + }, + { + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING, + "next", + &done_authentication + }, + { + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING, + "add_provider", + &add_provider + }, + { + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "add_policy", + &add_policy + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "update_policy", + &update_policy + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "delete_policy", + &del_policy + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "delete_challenge", + &del_challenge + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "next", + &done_policy_review + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "enter_secret", + &enter_secret + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "clear_secret", + &clear_secret + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "enter_secret_name", + &enter_secret_name + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "update_expiration", + &update_expiration + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "next", + &finish_secret + }, + { + ANASTASIS_BACKUP_STATE_TRUTHS_PAYING, + "pay", + &pay_truths_backup + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_PAYING, + "pay", + &pay_policies_backup + }, + { + ANASTASIS_BACKUP_STATE_BACKUP_FINISHED, + "back", + &back_finished + }, + { ANASTASIS_BACKUP_STATE_ERROR, NULL, NULL } + }; + const char *s = json_string_value (json_object_get (state, + "backup_state")); + enum ANASTASIS_BackupState bs; + + GNUNET_assert (NULL != s); /* holds as per invariant of caller */ + bs = ANASTASIS_backup_state_from_string_ (s); + if (ANASTASIS_BACKUP_STATE_ERROR == bs) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "unknown 'backup_state'"); + return NULL; + } + for (unsigned int i = 0; NULL != dispatchers[i].fun; i++) + { + if ( (bs == dispatchers[i].backup_state) && + (0 == strcmp (action, + dispatchers[i].backup_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 #ANASTASIS_REDUX_backup_begin_() operation. + */ +struct BackupStartState; + + +/** + * Entry in the list of all known applicable Anastasis providers. + * Used to wait for it to complete downloading /config. + */ +struct BackupStartStateProviderEntry +{ + /** + * Kept in a DLL. + */ + struct BackupStartStateProviderEntry *next; + + /** + * Kept in a DLL. + */ + struct BackupStartStateProviderEntry *prev; + + /** + * Main operation this entry is part of. + */ + struct BackupStartState *bss; + + /** + * 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; +}; + + +struct BackupStartState +{ + /** + * Head of list of provider /config operations we are doing. + */ + struct BackupStartStateProviderEntry *pe_head; + + /** + * Tail of list of provider /config operations we are doing. + */ + struct BackupStartStateProviderEntry *pe_tail; + + /** + * State we are updating. + */ + json_t *state; + + /** + * Function to call when we are done. + */ + ANASTASIS_ActionCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Redux action we returned to our controller. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * Number of provider /config operations in @e ba_head that + * are still awaiting completion. + */ + unsigned int pending; +}; + + +/** + * The backup start operation is being aborted, terminate. + * + * @param cls a `struct BackupStartState *` + */ +static void +abort_backup_begin_cb (void *cls) +{ + struct BackupStartState *bss = cls; + struct BackupStartStateProviderEntry *pe; + + while (NULL != (pe = bss->pe_head)) + { + GNUNET_CONTAINER_DLL_remove (bss->pe_head, + bss->pe_tail, + pe); + if (NULL != pe->ra) + pe->ra->cleanup (pe->ra->cleanup_cls); + json_decref (pe->istate); + GNUNET_free (pe); + } + json_decref (bss->state); + GNUNET_free (bss); +} + + +/** + * We finished downloading /config from all providers, merge + * into the main state, trigger the continuation and free our + * state. + * + * @param[in] bss main state to merge into + */ +static void +providers_complete (struct BackupStartState *bss) +{ + struct BackupStartStateProviderEntry *pe; + json_t *tlist; + + tlist = json_object_get (bss->state, + "authentication_providers"); + if (NULL == tlist) + { + tlist = json_object (); + GNUNET_assert (NULL != tlist); + GNUNET_assert (0 == + json_object_set_new (bss->state, + "authentication_providers", + tlist)); + } + while (NULL != (pe = bss->pe_head)) + { + json_t *provider_list; + + GNUNET_CONTAINER_DLL_remove (bss->pe_head, + bss->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); + } + bss->cb (bss->cb_cls, + TALER_EC_NONE, + bss->state); + json_decref (bss->state); + GNUNET_free (bss); +} + + +/** + * Function called when the complete information about a provider + * was added to @a new_state. + * + * @param cls a `struct BackupStartStateProviderEntry` + * @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 BackupStartStateProviderEntry *pe = cls; + + pe->ra = NULL; + pe->istate = json_incref (new_state); + pe->ec = error; + pe->bss->pending--; + if (0 == pe->bss->pending) + providers_complete (pe->bss); +} + + +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_backup_begin_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *provider_list; + struct BackupStartState *bss; + + provider_list = json_object_get (state, + "authentication_providers"); + if (NULL == provider_list) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_providers' missing"); + return NULL; + } + bss = GNUNET_new (struct BackupStartState); + bss->state = json_incref (state); + bss->cb = cb; + bss->cb_cls = cb_cls; + bss->ra.cleanup_cls = bss; + bss->ra.cleanup = &abort_backup_begin_cb; + bss->pending = 1; /* decremented after initialization loop */ + + { + json_t *prov; + const char *url; + json_object_foreach (provider_list, url, prov) { + struct BackupStartStateProviderEntry *pe; + json_t *istate; + + pe = GNUNET_new (struct BackupStartStateProviderEntry); + pe->bss = bss; + istate = json_object (); + GNUNET_assert (NULL != istate); + GNUNET_CONTAINER_DLL_insert (bss->pe_head, + bss->pe_tail, + pe); + pe->ra = ANASTASIS_REDUX_add_provider_to_state_ (url, + istate, + &provider_added_cb, + pe); + json_decref (istate); + if (NULL != pe->ra) + bss->pending++; + } + } + bss->pending--; + if (0 == bss->pending) + { + providers_complete (bss); + return NULL; + } + return &bss->ra; +} -- cgit v1.2.3