diff options
author | Christian Grothoff <christian@grothoff.org> | 2021-07-30 10:38:27 +0200 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2021-07-30 10:38:27 +0200 |
commit | 7e669bcf6b6336ec429da949bcb4aa456971dba2 (patch) | |
tree | d19912f950d1cac1c38b857b7d5bdaba2289544e /src/reducer | |
download | anastasis-7e669bcf6b6336ec429da949bcb4aa456971dba2.tar.gz anastasis-7e669bcf6b6336ec429da949bcb4aa456971dba2.tar.bz2 anastasis-7e669bcf6b6336ec429da949bcb4aa456971dba2.zip |
folding history in preparation of GNU Anastasis v0.0.0 release
Diffstat (limited to 'src/reducer')
-rw-r--r-- | src/reducer/Makefile.am | 45 | ||||
-rw-r--r-- | src/reducer/anastasis_api_backup_redux.c | 4893 | ||||
-rw-r--r-- | src/reducer/anastasis_api_recovery_redux.c | 2558 | ||||
-rw-r--r-- | src/reducer/anastasis_api_redux.c | 1730 | ||||
-rw-r--r-- | src/reducer/anastasis_api_redux.h | 347 | ||||
-rw-r--r-- | src/reducer/validation_CH_AHV.c | 57 | ||||
-rw-r--r-- | src/reducer/validation_CZ_BN.c | 59 | ||||
-rw-r--r-- | src/reducer/validation_DE_SVN.c | 98 | ||||
-rw-r--r-- | src/reducer/validation_DE_TIN.c | 57 | ||||
-rw-r--r-- | src/reducer/validation_IN_AADHAR.c | 113 | ||||
-rw-r--r-- | src/reducer/validation_IT_CF.c | 198 | ||||
-rw-r--r-- | src/reducer/validation_XX_SQUARE.c | 48 | ||||
-rw-r--r-- | src/reducer/validation_XY_PRIME.c | 53 |
13 files changed, 10256 insertions, 0 deletions
diff --git a/src/reducer/Makefile.am b/src/reducer/Makefile.am new file mode 100644 index 0000000..5cbe6f7 --- /dev/null +++ b/src/reducer/Makefile.am @@ -0,0 +1,45 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include -I$(top_srcdir)/src/backend -I$(top_srcdir)/src/lib + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +.NOTPARALLEL: + +lib_LTLIBRARIES = \ + libanastasisredux.la + +libanastasisredux_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined +libanastasisredux_la_SOURCES = \ + anastasis_api_redux.c anastasis_api_redux.h \ + anastasis_api_recovery_redux.c \ + anastasis_api_backup_redux.c \ + validation_CH_AHV.c \ + validation_CZ_BN.c \ + validation_DE_SVN.c \ + validation_DE_TIN.c \ + validation_IN_AADHAR.c \ + validation_IT_CF.c \ + validation_XX_SQUARE.c \ + validation_XY_PRIME.c +libanastasisredux_la_LIBADD = \ + $(top_builddir)/src/restclient/libanastasisrest.la \ + $(top_builddir)/src/lib/libanastasis.la \ + $(top_builddir)/src/util/libanastasisutil.la \ + -lgnunetjson \ + -lgnunetcurl \ + -lgnunetutil \ + -ltalermhd \ + -ltalerutil \ + -ltalerexchange \ + -ltalermerchant \ + -ltalerjson \ + -ljansson \ + -lgcrypt \ + -ldl \ + -lm \ + $(XLIB) 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 <http://www.gnu.org/licenses/> +*/ +/** + * @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 <taler/taler_merchant_service.h> + +/** + * 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; i<pb->req_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; k<pb->req_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; k<pb->req_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; j<pb->req_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; j2<pb->req_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; k<pb->req_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<off; i++) + if ( (seen[i].method == method_idx) && + (0 == strcmp (seen[i].provider_url, + provider_url)) ) + found = true; + if (found) + continue; /* skip */ + } + if (off == len) + { + GNUNET_array_grow (seen, + len, + 4 + len * 2); + } + seen[off].method = method_idx; + seen[off].provider_url = provider_url; + off++; + { + struct TALER_Amount upload_cost; + struct GNUNET_JSON_Specification pspec[] = { + TALER_JSON_spec_amount_any ("truth_upload_fee", + &upload_cost), + GNUNET_JSON_spec_end () + }; + struct TALER_Amount fee; + const json_t *provider_cfg + = json_object_get (providers, + provider_url); + + if (GNUNET_OK != + GNUNET_JSON_parse (provider_cfg, + pspec, + NULL, NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 > + 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; i<tue->policies_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; i<sr->details.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; i<policies_len; i++) + { + const json_t *policy = json_array_get (jpolicies, + i); + const json_t *jmethods = json_object_get (policy, + "methods"); + unsigned int methods_len; + + if ( (! json_is_array (jmethods)) || + (0 == json_array_size (jmethods)) ) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + 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; j<methods_len; j++) + { + const json_t *jmethod = json_array_get (jmethods, + j); + json_t *jtruth = NULL; + uint32_t truth_index; + const char *provider_url; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("truth", + &jtruth)), + GNUNET_JSON_spec_string ("provider", + &provider_url), + GNUNET_JSON_spec_uint32 ("authentication_method", + &truth_index), + GNUNET_JSON_spec_end () + }; + + GNUNET_break (NULL != jmethod); + if (GNUNET_OK != + GNUNET_JSON_parse (jmethod, + ispec, + NULL, NULL)) + { + GNUNET_break (0); + for (unsigned int k = 0; k<j; k++) + ANASTASIS_truth_free (truths[k]); + ANASTASIS_redux_fail_ (uc->cb, + 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; k<j; k++) + ANASTASIS_truth_free (truths[k]); + ANASTASIS_redux_fail_ (uc->cb, + 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; k<j; k++) + ANASTASIS_truth_free (truths[k]); + ANASTASIS_redux_fail_ (uc->cb, + 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; k<methods_len; k++) + ANASTASIS_truth_free (truths[k]); + } + } + + /* initialize 'pds' array */ + for (unsigned int i = 0; i<pds_len; i++) + { + json_t *pdj = json_array_get (providers, + i); + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &pds[i].payment_secret)), + GNUNET_JSON_spec_string ("provider_url", + &pds[i].provider_url), + GNUNET_JSON_spec_end () + }; + + if ( (GNUNET_OK != + GNUNET_JSON_parse (pdj, + ispec, + NULL, NULL)) || + (GNUNET_OK != + lookup_salt (uc->state, + 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; i<policies_len; i++) + ANASTASIS_policy_destroy (vpolicies[i]); + upload_cancel_cb (uc); + GNUNET_JSON_parse_free (spec); + return; + } + } + + { + char *secret; + size_t secret_size; + + secret = json_dumps (core_secret, + JSON_COMPACT | JSON_SORT_KEYS); + GNUNET_assert (NULL != secret); + secret_size = strlen (secret); + uc->ss = 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; i<policies_len; i++) + ANASTASIS_policy_destroy (vpolicies[i]); + } + GNUNET_JSON_parse_free (spec); + if (NULL == uc->ss) + { + 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; +} diff --git a/src/reducer/anastasis_api_recovery_redux.c b/src/reducer/anastasis_api_recovery_redux.c new file mode 100644 index 0000000..8a900ec --- /dev/null +++ b/src/reducer/anastasis_api_recovery_redux.c @@ -0,0 +1,2558 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 2021 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/anastasis_api_recovery_redux.c + * @brief anastasis reducer recovery api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ + +#include <platform.h> +#include <jansson.h> +#include "anastasis_redux.h" +#include "anastasis_error_codes.h" +#include "anastasis_api_redux.h" + + +#define GENERATE_STRING(STRING) #STRING, +static const char *recovery_strings[] = { + ANASTASIS_RECOVERY_STATES (GENERATE_STRING) +}; +#undef GENERATE_STRING + + +enum ANASTASIS_RecoveryState +ANASTASIS_recovery_state_from_string_ (const char *state_string) +{ + for (enum ANASTASIS_RecoveryState i = 0; + i < sizeof (recovery_strings) / sizeof(*recovery_strings); + i++) + if (0 == strcmp (state_string, + recovery_strings[i])) + return i; + return ANASTASIS_RECOVERY_STATE_ERROR; +} + + +const char * +ANASTASIS_recovery_state_to_string_ (enum ANASTASIS_RecoveryState rs) +{ + if ( (rs < 0) || + (rs >= sizeof (recovery_strings) / sizeof(*recovery_strings)) ) + { + GNUNET_break_op (0); + return NULL; + } + return recovery_strings[rs]; +} + + +static void +set_state (json_t *state, + enum ANASTASIS_RecoveryState new_recovery_state) +{ + GNUNET_assert ( + 0 == + json_object_set_new ( + state, + "recovery_state", + json_string (ANASTASIS_recovery_state_to_string_ (new_recovery_state)))); +} + + +/** + * Returns an initial ANASTASIS recovery state. + * + * @return NULL on failure + */ +json_t * +ANASTASIS_recovery_start (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + json_t *initial_state; + + (void) cfg; + initial_state = ANASTASIS_REDUX_load_continents_ (); + if (NULL == initial_state) + return NULL; + set_state (initial_state, + ANASTASIS_RECOVERY_STATE_CONTINENT_SELECTING); + return initial_state; +} + + +/** + * Context for a "select_challenge" operation. + */ +struct SelectChallengeContext +{ + /** + * Handle we returned for cancellation of the operation. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * UUID of the challenge selected by the user for solving. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + + /** + * Which timeout was set for the operation? + */ + struct GNUNET_TIME_Relative timeout; + + /** + * Overall recovery action. + */ + struct ANASTASIS_Recovery *r; + + /** + * Function to call with the next state. + */ + ANASTASIS_ActionCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Our state. + */ + json_t *state; + + /** + * Our arguments (like answers to the challenge, if already provided). + */ + json_t *args; + + /** + * Task scheduled for delayed success reporting. Needed to make + * sure that the solved challenge was really the final result, + * cancelled if the solved challenge resulted in the secret being + * recovered. + */ + struct GNUNET_SCHEDULER_Task *delayed_report; + + /** + * Payment secret, if we are in the "pay" state. + */ + struct ANASTASIS_PaymentSecretP ps; +}; + + +/** + * Cleanup a select challenge context. + * + * @param cls a `struct SelectChallengeContext *` + */ +static void +sctx_free (void *cls) +{ + struct SelectChallengeContext *sctx = cls; + + if (NULL != sctx->r) + { + ANASTASIS_recovery_abort (sctx->r); + sctx->r = NULL; + } + json_decref (sctx->state); + json_decref (sctx->args); + if (NULL != sctx->delayed_report) + { + GNUNET_SCHEDULER_cancel (sctx->delayed_report); + sctx->delayed_report = NULL; + } + GNUNET_free (sctx); +} + + +/** + * Update @a state to reflect the error provided in @a rc. + * + * @param[in,out] state state to update + * @param rc error code to translate to JSON + * @return error code to use + */ +static enum TALER_ErrorCode +update_state_by_error (json_t *state, + enum ANASTASIS_RecoveryStatus rc) +{ + const char *msg = NULL; + enum TALER_ErrorCode ec = TALER_EC_INVALID; + + switch (rc) + { + case ANASTASIS_RS_SUCCESS: + GNUNET_assert (0); + break; + case ANASTASIS_RS_POLICY_DOWNLOAD_FAILED: + msg = gettext_noop ("download failed due to unexpected network issue"); + ec = TALER_EC_ANASTASIS_REDUCER_NETWORK_FAILED; + break; + case ANASTASIS_RS_POLICY_DOWNLOAD_NO_POLICY: + GNUNET_break (0); + msg = gettext_noop ("policy document returned was malformed"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_POLICY_DOWNLOAD_TOO_BIG: + GNUNET_break (0); + msg = gettext_noop ("policy document too large for client memory"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_POLICY_DOWNLOAD_INVALID_COMPRESSION: + GNUNET_break (0); + msg = gettext_noop ("failed to decompress policy document"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_POLICY_DOWNLOAD_NO_JSON: + GNUNET_break (0); + msg = gettext_noop ("policy document returned was not in JSON format"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_POLICY_MALFORMED_JSON: + GNUNET_break (0); + msg = gettext_noop ( + "policy document returned was not in required JSON format"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_POLICY_SERVER_ERROR: + msg = gettext_noop ("Anastasis server reported transient internal error"); + ec = TALER_EC_ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED; + break; + case ANASTASIS_RS_POLICY_GONE: + msg = gettext_noop ("policy document no longer exists"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED; + break; + case ANASTASIS_RS_POLICY_UNKNOWN: + msg = gettext_noop ("account unknown to Anastasis server"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED; + break; + } + GNUNET_assert (0 == + json_object_set_new (state, + "error_message", + json_string (msg))); + GNUNET_assert (0 == + json_object_set_new (state, + "error_code", + json_integer (rc))); + set_state (state, + ANASTASIS_GENERIC_STATE_ERROR); + return ec; +} + + +/** + * This function is called whenever the recovery process ends. + * On success, the secret is returned in @a secret. + * + * @param cls handle for the callback + * @param ec error code + * @param secret contains the core secret which is passed to the user + * @param secret_size defines the size of the core secret + */ +static void +core_secret_cb (void *cls, + enum ANASTASIS_RecoveryStatus rc, + const void *secret, + size_t secret_size) +{ + struct SelectChallengeContext *sctx = cls; + enum TALER_ErrorCode ec; + + sctx->r = NULL; + if (ANASTASIS_RS_SUCCESS == rc) + { + json_t *jsecret; + + jsecret = json_loadb (secret, + secret_size, + JSON_REJECT_DUPLICATES, + NULL); + if (NULL == jsecret) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_SECRET_MALFORMED, + NULL); + sctx_free (sctx); + return; + } + GNUNET_assert (0 == + json_object_set_new (sctx->state, + "core_secret", + jsecret)); + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_RECOVERY_FINISHED); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + } + ec = update_state_by_error (sctx->state, + rc); + sctx->cb (sctx->cb_cls, + ec, + sctx->state); + sctx_free (sctx); +} + + +/** + * A challenge was solved, but we are not yet finished. + * Report to caller that the challenge was completed. + * + * @param cls a `struct SelectChallengeContext` + */ +static void +report_solved (void *cls) +{ + struct SelectChallengeContext *sctx = cls; + + sctx->delayed_report = NULL; + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); +} + + +/** + * Find challenge of @a uuid in @a state under "recovery_information". + * + * @param state the state to search + * @param uuid the UUID to search for + * @return NULL on error, otherwise challenge entry; RC is NOT incremented + */ +static json_t * +find_challenge_in_ri (json_t *state, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid) +{ + struct ANASTASIS_CRYPTO_TruthUUIDP u; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("uuid", + &u), + GNUNET_JSON_spec_end () + }; + json_t *ri; + json_t *challenges; + json_t *challenge; + size_t index; + + ri = json_object_get (state, + "recovery_information"); + if (NULL == ri) + { + GNUNET_break (0); + return NULL; + } + challenges = json_object_get (ri, + "challenges"); + if (NULL == challenges) + { + GNUNET_break (0); + return NULL; + } + json_array_foreach (challenges, index, challenge) + { + if (GNUNET_OK != + GNUNET_JSON_parse (challenge, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + if (0 == + GNUNET_memcmp (&u, + uuid)) + { + return challenge; + } + } + return NULL; +} + + +/** + * Defines a callback for the response status for a challenge start + * operation. + * + * @param cls a `struct SelectChallengeContext *` + * @param csr response details + */ +static void +answer_feedback_cb ( + void *cls, + const struct ANASTASIS_ChallengeStartResponse *csr) +{ + struct SelectChallengeContext *sctx = cls; + const struct ANASTASIS_ChallengeDetails *cd; + char uuid[sizeof (cd->uuid) * 2]; + char *end; + json_t *feedback; + + cd = ANASTASIS_challenge_get_details (csr->challenge); + end = GNUNET_STRINGS_data_to_string (&cd->uuid, + sizeof (cd->uuid), + uuid, + sizeof (uuid)); + GNUNET_assert (NULL != end); + *end = '\0'; + feedback = json_object_get (sctx->state, + "challenge_feedback"); + if (NULL == feedback) + { + feedback = json_object (); + GNUNET_assert (0 == + json_object_set_new (sctx->state, + "challenge_feedback", + feedback)); + } + switch (csr->cs) + { + case ANASTASIS_CHALLENGE_STATUS_SOLVED: + { + json_t *rd; + + rd = ANASTASIS_recovery_serialize (sctx->r); + if (NULL == rd) + { + GNUNET_break (0); + set_state (sctx->state, + ANASTASIS_GENERIC_STATE_ERROR); + sctx->cb (sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + sctx->state); + sctx_free (sctx); + return; + } + GNUNET_assert (0 == + json_object_set_new (sctx->state, + "recovery_document", + rd)); + } + { + json_t *solved; + + solved = json_pack ("{s:s}", + "state", + "solved"); + GNUNET_assert (NULL != solved); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + solved)); + } + /* Delay reporting challenge success, as we MAY still + also see a secret recovery success (and we can only + call the callback once) */ + sctx->delayed_report = GNUNET_SCHEDULER_add_now (&report_solved, + sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_INSTRUCTIONS: + { + json_t *instructions; + json_t *val; + const char *mime; + + mime = csr->details.open_challenge.content_type; + if (NULL != mime) + { + if ( (0 == strcasecmp (mime, + "text/plain")) || + (0 == strcasecmp (mime, + "text/utf8")) ) + { + char *s = GNUNET_strndup (csr->details.open_challenge.body, + csr->details.open_challenge.body_size); + + instructions = json_pack ( + "{s:s, s:s, s:I}", + "state", + "hint", + "hint", + s, + "http_status", + (json_int_t) csr->details.open_challenge.http_status); + GNUNET_free (s); + } + else if (0 == strcasecmp (mime, + "application/json")) + { + json_t *body; + + body = json_loadb (csr->details.open_challenge.body, + csr->details.open_challenge.body_size, + JSON_REJECT_DUPLICATES, + NULL); + if (NULL == body) + { + GNUNET_break_op (0); + mime = NULL; + } + else + { + instructions = json_pack ( + "{s:s, s:o, s:I}", + "state", + "details", + "details", + body, + "http_status", + (json_int_t) csr->details.open_challenge.http_status); + } + } + else + { + /* unexpected / unsupported mime type */ + mime = NULL; + } + } + if (NULL == mime) + { + val = GNUNET_JSON_from_data (csr->details.open_challenge.body, + csr->details.open_challenge.body_size); + GNUNET_assert (NULL != val); + instructions = json_pack ( + "{s:s, s:o, s:I, s:s?}", + "state", + "body", + "body", + val, + "http_status", + (json_int_t) csr->details.open_challenge.http_status, + "mime_type", + mime); + } + GNUNET_assert (NULL != instructions); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + instructions)); + } + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_REDIRECT_FOR_AUTHENTICATION: + { + json_t *redir; + + redir = json_pack ("{s:s, s:s}", + "state", + "redirect", + "redirect_url", + csr->details.redirect_url); + GNUNET_assert (NULL != redir); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + redir)); + } + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_PAYMENT_REQUIRED: + { + json_t *pay; + + pay = json_pack ("{s:s, s:s, s:s, s:o}", + "state", + "payment", + "taler_pay_uri", + csr->details.payment_required.taler_pay_uri, + "provider", + cd->provider_url, + "payment_secret", + GNUNET_JSON_from_data_auto ( + &csr->details.payment_required.payment_secret)); + GNUNET_assert (NULL != pay); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + pay)); + } + /* Remember payment secret for later (once application claims it paid) */ + { + json_t *challenge = find_challenge_in_ri (sctx->state, + &cd->uuid); + + GNUNET_assert (NULL != challenge); + GNUNET_assert (0 == + json_object_set_new (challenge, + "payment_secret", + GNUNET_JSON_from_data_auto ( + &csr->details.payment_required. + payment_secret))); + } + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_SERVER_FAILURE: + { + json_t *err; + + err = json_pack ("{s:s, s:I, s:I}", + "state", + "server-failure", + "http_status", + (json_int_t) csr->details.server_failure.http_status, + "error_code", + (json_int_t) csr->details.server_failure.ec); + GNUNET_assert (NULL != err); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + err)); + } + GNUNET_break_op (0); + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_TRUTH_UNKNOWN: + { + json_t *err; + + err = json_pack ("{s:s, s:I}", + "state", + "truth-unknown", + "error_code", + (json_int_t) TALER_EC_ANASTASIS_TRUTH_UNKNOWN); + GNUNET_assert (NULL != err); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + err)); + } + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_RATE_LIMIT_EXCEEDED: + { + json_t *err; + + err = json_pack ("{s:s, s:I}", + "state", + "rate-limit-exceeded", + "error_code", + (json_int_t) TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED); + + GNUNET_assert (NULL != err); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + err)); + } + GNUNET_break_op (0); + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + } + GNUNET_break (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + sctx_free (sctx); +} + + +/** + * Callback which passes back the recovery document and its possible + * policies. Also passes back the version of the document for the user + * to check. + * + * We find the selected challenge and try to answer it (or begin + * the process). + * + * @param cls a `struct SelectChallengeContext *` + * @param ri recovery information struct which contains the policies + */ +static void +solve_challenge_cb (void *cls, + const struct ANASTASIS_RecoveryInformation *ri) +{ + struct SelectChallengeContext *sctx = cls; + const struct ANASTASIS_PaymentSecretP *psp = NULL; + struct ANASTASIS_PaymentSecretP ps; + struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; + struct GNUNET_JSON_Specification tspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &timeout)), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &ps), + GNUNET_JSON_spec_end () + }; + + json_t *challenge; + + if (NULL == ri) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "recovery information could not be deserialized"); + sctx_free (sctx); + return; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (sctx->args, + tspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'timeout' malformed"); + sctx_free (sctx); + return; + } + + /* Check if we got a payment_secret */ + challenge = find_challenge_in_ri (sctx->state, + &sctx->uuid); + if (NULL == challenge) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "challenge not found"); + sctx_free (sctx); + return; + } + + if (NULL != + json_object_get (sctx->args, + "payment_secret")) + { + /* check if we got payment secret in args */ + if (GNUNET_OK != + GNUNET_JSON_parse (sctx->args, + pspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'payment_secret' malformed"); + sctx_free (sctx); + return; + } + psp = &ps; + } + else if (NULL != + json_object_get (challenge, + "payment_secret")) + { + if (GNUNET_OK != + GNUNET_JSON_parse (challenge, + pspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'payment_secret' malformed"); + sctx_free (sctx); + return; + } + psp = &ps; + } + + for (unsigned int i = 0; i<ri->cs_len; i++) + { + struct ANASTASIS_Challenge *ci = ri->cs[i]; + const struct ANASTASIS_ChallengeDetails *cd; + int ret; + + cd = ANASTASIS_challenge_get_details (ci); + if (0 != + GNUNET_memcmp (&sctx->uuid, + &cd->uuid)) + continue; + if (cd->solved) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "Selected challenge already solved"); + sctx_free (sctx); + return; + } + if (0 == strcmp ("question", + cd->type)) + { + /* security question, answer must be a string */ + json_t *janswer = json_object_get (sctx->args, + "answer"); + const char *answer = json_string_value (janswer); + + if (NULL == answer) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'answer' missing"); + sctx_free (sctx); + return; + } + /* persist answer, in case payment is required */ + GNUNET_assert (0 == + json_object_set (challenge, + "answer", + janswer)); + ret = ANASTASIS_challenge_answer (ci, + psp, + timeout, + answer, + &answer_feedback_cb, + sctx); + } + else + { + /* Check if we got a PIN or a HASH */ + json_t *pin = json_object_get (sctx->args, + "pin"); + json_t *hash = json_object_get (sctx->args, + "hash"); + if (json_is_integer (pin)) + { + uint64_t ianswer = json_integer_value (pin); + + ret = ANASTASIS_challenge_answer2 (ci, + psp, + timeout, + ianswer, + &answer_feedback_cb, + sctx); + } + else if (NULL != hash) + { + struct GNUNET_HashCode hashed_answer; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("hash", + &hashed_answer), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (sctx->args, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'answer' malformed"); + sctx_free (sctx); + return; + } + ret = ANASTASIS_challenge_start (ci, + psp, + timeout, + &hashed_answer, + &answer_feedback_cb, + sctx); + } + else + { + /* no answer provided */ + ret = ANASTASIS_challenge_start (ci, + psp, + timeout, + NULL, /* no answer */ + &answer_feedback_cb, + sctx); + } + } + if (GNUNET_OK != ret) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Failed to begin answering challenge"); + sctx_free (sctx); + return; + } + return; /* await answer feedback */ + } + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'uuid' not in list of challenges"); + sctx_free (sctx); +} + + +/** + * Callback which passes back the recovery document and its possible + * policies. Also passes back the version of the document for the user + * to check. + * + * We find the selected challenge and try to answer it (or begin + * the process). + * + * @param cls a `struct SelectChallengeContext *` + * @param ri recovery information struct which contains the policies + */ +static void +pay_challenge_cb (void *cls, + const struct ANASTASIS_RecoveryInformation *ri) +{ + struct SelectChallengeContext *sctx = cls; + json_t *challenge; + + if (NULL == ri) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "recovery information could not be deserialized"); + sctx_free (sctx); + return; + } + + challenge = find_challenge_in_ri (sctx->state, + &sctx->uuid); + if (NULL == challenge) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "challenge not found"); + sctx_free (sctx); + return; + } + /* persist payment, in case we need to run the request again */ + GNUNET_assert ( + 0 == + json_object_set_new (challenge, + "payment_secret", + GNUNET_JSON_from_data_auto (&sctx->ps))); + + for (unsigned int i = 0; i<ri->cs_len; i++) + { + struct ANASTASIS_Challenge *ci = ri->cs[i]; + const struct ANASTASIS_ChallengeDetails *cd; + int ret; + + cd = ANASTASIS_challenge_get_details (ci); + if (0 != + GNUNET_memcmp (&sctx->uuid, + &cd->uuid)) + continue; + if (cd->solved) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "Selected challenge already solved"); + sctx_free (sctx); + return; + } + + if (0 == strcmp ("question", + cd->type)) + { + /* security question, answer must be a string and already ready */ + json_t *janswer = json_object_get (challenge, + "answer"); + const char *answer = json_string_value (janswer); + + if (NULL == answer) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'answer' missing"); + sctx_free (sctx); + return; + } + ret = ANASTASIS_challenge_answer (ci, + &sctx->ps, + sctx->timeout, + answer, + &answer_feedback_cb, + sctx); + } + else + { + ret = ANASTASIS_challenge_start (ci, + &sctx->ps, + sctx->timeout, + NULL, /* no answer yet */ + &answer_feedback_cb, + sctx); + } + if (GNUNET_OK != ret) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Failed to begin answering challenge"); + sctx_free (sctx); + return; + } + return; /* await answer feedback */ + } + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'uuid' not in list of challenges"); + sctx_free (sctx); +} + + +/** + * The user selected a challenge to be solved. Begin the solving + * process. + * + * @param[in] state we are in + * @param arguments our arguments with the solution + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return handle to cancel challenge selection step + */ +static struct ANASTASIS_ReduxAction * +solve_challenge (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct SelectChallengeContext *sctx + = GNUNET_new (struct SelectChallengeContext); + json_t *rd; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("selected_challenge_uuid", + &sctx->uuid), + GNUNET_JSON_spec_end () + }; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'selected_challenge_uuid' missing"); + return NULL; + } + rd = json_object_get (state, + "recovery_document"); + if (NULL == rd) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "solve_challenge"); + return NULL; + } + sctx->cb = cb; + sctx->cb_cls = cb_cls; + sctx->state = json_incref (state); + sctx->args = json_incref ((json_t*) arguments); + sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, + rd, + &solve_challenge_cb, + sctx, + &core_secret_cb, + sctx); + if (NULL == sctx->r) + { + json_decref (sctx->state); + json_decref (sctx->args); + GNUNET_free (sctx); + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'recovery_document' invalid"); + return NULL; + } + sctx->ra.cleanup = &sctx_free; + sctx->ra.cleanup_cls = sctx; + return &sctx->ra; +} + + +/** + * The user selected a challenge to be solved. Handle the payment + * process. + * + * @param[in] state we are in + * @param arguments our arguments with the solution + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return handle to cancel challenge selection step + */ +static struct ANASTASIS_ReduxAction * +pay_challenge (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct SelectChallengeContext *sctx + = GNUNET_new (struct SelectChallengeContext); + json_t *rd; + struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("selected_challenge_uuid", + &sctx->uuid), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification aspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &timeout)), + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &sctx->ps), + GNUNET_JSON_spec_end () + }; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (arguments, + aspec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'payment_secret' missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'selected_challenge_uuid' missing"); + return NULL; + } + rd = json_object_get (state, + "recovery_document"); + if (NULL == rd) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "pay_challenge"); + return NULL; + } + sctx->timeout = timeout; + sctx->cb = cb; + sctx->cb_cls = cb_cls; + sctx->state = json_incref (state); + sctx->args = json_incref ((json_t*) arguments); + sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, + rd, + &pay_challenge_cb, + sctx, + &core_secret_cb, + sctx); + if (NULL == sctx->r) + { + json_decref (sctx->state); + json_decref (sctx->args); + GNUNET_free (sctx); + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'recovery_document' invalid"); + return NULL; + } + sctx->ra.cleanup = &sctx_free; + sctx->ra.cleanup_cls = sctx; + return &sctx->ra; +} + + +/** + * Callback which passes back the recovery document and its possible + * policies. Also passes back the version of the document for the user + * to check. + * + * We find the selected challenge and try to answer it (or begin + * the process). + * + * @param cls a `struct SelectChallengeContext *` + * @param ri recovery information struct which contains the policies + */ +static void +select_challenge_cb (void *cls, + const struct ANASTASIS_RecoveryInformation *ri) +{ + struct SelectChallengeContext *sctx = cls; + const struct ANASTASIS_PaymentSecretP *psp = NULL; + struct ANASTASIS_PaymentSecretP ps; + struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; + struct GNUNET_JSON_Specification tspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &timeout)), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &ps), + GNUNET_JSON_spec_end () + }; + + + if (NULL == ri) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "recovery information could not be deserialized"); + sctx_free (sctx); + return; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (sctx->args, + tspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'timeout' malformed"); + sctx_free (sctx); + return; + } + + /* NOTE: do we need both ways to pass payment secrets? */ + if (NULL != + json_object_get (sctx->args, + "payment_secret")) + { + /* check if we got payment secret in args */ + if (GNUNET_OK != + GNUNET_JSON_parse (sctx->args, + pspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'payment_secret' malformed"); + sctx_free (sctx); + return; + } + psp = &ps; + } + else + { + /* Check if we got a payment_secret in state */ + json_t *challenge = find_challenge_in_ri (sctx->state, + &sctx->uuid); + + if (NULL == challenge) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "challenge not found"); + sctx_free (sctx); + return; + } + if (NULL != + json_object_get (challenge, + "payment_secret")) + { + if (GNUNET_OK != + GNUNET_JSON_parse (challenge, + pspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'payment_secret' malformed"); + sctx_free (sctx); + return; + } + psp = &ps; + } + } + + for (unsigned int i = 0; i<ri->cs_len; i++) + { + struct ANASTASIS_Challenge *ci = ri->cs[i]; + const struct ANASTASIS_ChallengeDetails *cd; + int ret; + + cd = ANASTASIS_challenge_get_details (ci); + if (0 != + GNUNET_memcmp (&sctx->uuid, + &cd->uuid)) + continue; + if (cd->solved) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "Selected challenge already solved"); + sctx_free (sctx); + return; + } + GNUNET_assert ( + 0 == + json_object_set_new (sctx->state, + "selected_challenge_uuid", + GNUNET_JSON_from_data_auto (&cd->uuid))); + if (0 == strcmp ("question", + cd->type)) + { + /* security question, immediately request user to answer it */ + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + } + /* trigger challenge */ + ret = ANASTASIS_challenge_start (ci, + psp, + timeout, + NULL, /* no answer */ + &answer_feedback_cb, + sctx); + if (GNUNET_OK != ret) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Failed to begin answering challenge"); + sctx_free (sctx); + return; + } + return; /* await answer feedback */ + } + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'uuid' not in list of challenges"); + sctx_free (sctx); +} + + +/** + * The user selected a challenge to be solved. Begin the solving + * process. + * + * @param[in] state we are in + * @param arguments our arguments with the solution + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return handle to cancel challenge selection step + */ +static struct ANASTASIS_ReduxAction * +select_challenge (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct SelectChallengeContext *sctx + = GNUNET_new (struct SelectChallengeContext); + json_t *rd; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("uuid", + &sctx->uuid), + GNUNET_JSON_spec_end () + }; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (arguments, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'uuid' missing"); + return NULL; + } + rd = json_object_get (state, + "recovery_document"); + if (NULL == rd) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "select_challenge"); + return NULL; + } + sctx->cb = cb; + sctx->cb_cls = cb_cls; + sctx->state = json_incref (state); + sctx->args = json_incref ((json_t*) arguments); + sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, + rd, + &select_challenge_cb, + sctx, + &core_secret_cb, + sctx); + if (NULL == sctx->r) + { + json_decref (sctx->state); + json_decref (sctx->args); + GNUNET_free (sctx); + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'recovery_document' invalid"); + return NULL; + } + sctx->ra.cleanup = &sctx_free; + sctx->ra.cleanup_cls = sctx; + return &sctx->ra; +} + + +/** + * The user pressed "back" during challenge solving. + * Transition back to selecting another challenge. + * + * @param[in] state we are in + * @param arguments our arguments (unused) + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return NULL (synchronous operation) + */ +static struct ANASTASIS_ReduxAction * +back_challenge_solving (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + (void) arguments; + GNUNET_assert (0 == + json_object_del (state, + "selected_challenge_uuid")); + set_state (state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * The user wants us to change the policy version. Download another version. + * + * @param[in] state we are in + * @param arguments our arguments with the solution + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return handle to cancel challenge selection step + */ +static struct ANASTASIS_ReduxAction * +change_version (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + uint64_t version; + const char *provider_url; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint64 ("version", + &version), + GNUNET_JSON_spec_string ("provider_url", + &provider_url), + GNUNET_JSON_spec_end () + }; + json_t *ia; + json_t *args; + struct ANASTASIS_ReduxAction *ra; + + if (GNUNET_OK != + GNUNET_JSON_parse (arguments, + spec, + NULL, NULL)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'version' invalid"); + return NULL; + } + GNUNET_assert (NULL != provider_url); + ia = json_object_get (state, + "identity_attributes"); + if (NULL == ia) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'identity_attributes' missing"); + return NULL; + } + args = json_pack ("{s:I, s:O, s:s}", + "version", + (json_int_t) version, + "identity_attributes", + ia, + "provider_url", + provider_url); + if (NULL == args) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + NULL); + return NULL; + } + ra = ANASTASIS_REDUX_recovery_challenge_begin_ (state, + args, + cb, + cb_cls); + json_decref (args); + return ra; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "next" action in "secret_selecting" state. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +done_secret_selecting (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *ri; + + ri = json_object_get (state, + "recovery_information"); + if ( (NULL == ri) || + (NULL == json_object_get (ri, + "challenges")) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "no valid version selected"); + return NULL; + } + set_state (state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * Signature of callback function that implements a state transition. + * + * @param state current state + * @param arguments arguments for the state transition + * @param cb function to call when done + * @param cb_cls closure for @a cb + */ +typedef struct ANASTASIS_ReduxAction * +(*DispatchHandler)(json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * Operates on a recovery state depending on given #ANASTASIS_RecoveryState + * and #ANASTASIS_RecoveryAction. The new #ANASTASIS_RecoveryState is returned + * by a callback function. + * This function can do network access to talk to anastasis service providers. + * + * @param[in,out] state input/output state (to be modified) + * @param action what action to perform + * @param arguments data for the @a action + * @param cb function to call with the result + * @param cb_cls closure for @a cb + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_recovery_action_ (json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct Dispatcher + { + enum ANASTASIS_RecoveryState recovery_state; + const char *recovery_action; + DispatchHandler fun; + } dispatchers[] = { + { + ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, + "change_version", + &change_version + }, + { + ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, + "next", + &done_secret_selecting + }, + { + ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING, + "select_challenge", + &select_challenge + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING, + "pay", + &pay_challenge + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING, + "solve_challenge", + &solve_challenge + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING, + "back", + &back_challenge_solving + }, + { ANASTASIS_RECOVERY_STATE_ERROR, NULL, NULL } + }; + const char *s = json_string_value (json_object_get (state, + "recovery_state")); + enum ANASTASIS_RecoveryState rs; + + GNUNET_assert (NULL != s); + rs = ANASTASIS_recovery_state_from_string_ (s); + if (ANASTASIS_RECOVERY_STATE_ERROR == rs) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'recovery_state' field invalid"); + return NULL; + } + for (unsigned int i = 0; NULL != dispatchers[i].fun; i++) + { + if ( (rs == dispatchers[i].recovery_state) && + (0 == strcmp (action, + dispatchers[i].recovery_action)) ) + { + return dispatchers[i].fun (state, + arguments, + cb, + cb_cls); + } + } + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID, + action); + return NULL; +} + + +/** + * State for a "recover secret" CMD. + */ +struct RecoverSecretState; + + +/** + * State for a "policy download" as part of a recovery operation. + */ +struct PolicyDownloadEntry +{ + + /** + * Kept in a DLL. + */ + struct PolicyDownloadEntry *prev; + + /** + * Kept in a DLL. + */ + struct PolicyDownloadEntry *next; + + /** + * Backend we are querying. + */ + char *backend_url; + + /** + * Salt to be used to derive the id for this provider + */ + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + + /** + * Context we operate in. + */ + struct RecoverSecretState *rss; + + /** + * The /policy GET operation handle. + */ + struct ANASTASIS_Recovery *recovery; + +}; + + +/** + * Entry in the list of all known applicable Anastasis providers. + * Used to wait for it to complete downloading /config. + */ +struct RecoveryStartStateProviderEntry +{ + /** + * Kept in a DLL. + */ + struct RecoveryStartStateProviderEntry *next; + + /** + * Kept in a DLL. + */ + struct RecoveryStartStateProviderEntry *prev; + + /** + * Main operation this entry is part of. + */ + struct RecoverSecretState *rss; + + /** + * Resulting provider information, NULL if not (yet) available. + */ + json_t *istate; + + /** + * Ongoing reducer action to obtain /config, NULL if completed. + */ + struct ANASTASIS_ReduxAction *ra; + + /** + * Final result of the operation (once completed). + */ + enum TALER_ErrorCode ec; +}; + + +/** + * State for a "recover secret" CMD. + */ +struct RecoverSecretState +{ + + /** + * Redux action handle associated with this state. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * Head of list of provider /config operations we are doing. + */ + struct RecoveryStartStateProviderEntry *pe_head; + + /** + * Tail of list of provider /config operations we are doing. + */ + struct RecoveryStartStateProviderEntry *pe_tail; + + /** + * Identification data from the user + */ + json_t *id_data; + + /** + * Head of DLL of policy downloads. + */ + struct PolicyDownloadEntry *pd_head; + + /** + * Tail of DLL of policy downloads. + */ + struct PolicyDownloadEntry *pd_tail; + + /** + * Reference to our state. + */ + json_t *state; + + /** + * callback to call during/after operation + */ + ANASTASIS_ActionCallback cb; + + /** + * closure for action callback @e cb. + */ + void *cb_cls; + + /** + * Set if recovery must be done with this provider. + */ + char *provider_url; + + /** + * version of the recovery document to request. + */ + unsigned int version; + + /** + * Number of provider /config operations in @e ba_head that + * are still awaiting completion. + */ + unsigned int pending; + + /** + * Is @e version set? + */ + bool have_version; +}; + + +/** + * Function to free a #RecoverSecretState. + * + * @param cls closure for a #RecoverSecretState. + */ +static void +free_rss (void *cls) +{ + struct RecoverSecretState *rss = cls; + struct PolicyDownloadEntry *pd; + struct RecoveryStartStateProviderEntry *pe; + + while (NULL != (pe = rss->pe_head)) + { + GNUNET_CONTAINER_DLL_remove (rss->pe_head, + rss->pe_tail, + pe); + ANASTASIS_redux_action_cancel (pe->ra); + rss->pending--; + GNUNET_free (pe); + } + while (NULL != (pd = rss->pd_head)) + { + GNUNET_CONTAINER_DLL_remove (rss->pd_head, + rss->pd_tail, + pd); + if (NULL != pd->recovery) + { + ANASTASIS_recovery_abort (pd->recovery); + pd->recovery = NULL; + } + GNUNET_free (pd->backend_url); + GNUNET_free (pd); + } + json_decref (rss->state); + json_decref (rss->id_data); + GNUNET_assert (0 == rss->pending); + GNUNET_free (rss->provider_url); + GNUNET_free (rss); +} + + +/** + * This function is called whenever the recovery process ends. + * In this case, that should not be possible as this callback + * is used before we even begin with the challenges. So if + * we are called, it is because of some fatal error. + * + * @param cls a `struct PolicyDownloadEntry` + * @param ec error code + * @param secret contains the core secret which is passed to the user + * @param secret_size defines the size of the core secret + */ +static void +core_early_secret_cb (void *cls, + enum ANASTASIS_RecoveryStatus rc, + const void *secret, + size_t secret_size) +{ + struct PolicyDownloadEntry *pd = cls; + struct RecoverSecretState *rss = pd->rss; + enum TALER_ErrorCode ec; + + pd->recovery = NULL; + GNUNET_assert (NULL == secret); + GNUNET_CONTAINER_DLL_remove (rss->pd_head, + rss->pd_tail, + pd); + GNUNET_free (pd->backend_url); + GNUNET_free (pd); + if (NULL != rss->pd_head) + return; /* wait for another one */ + /* all failed! report failure! */ + GNUNET_assert (ANASTASIS_RS_SUCCESS != rc); + ec = update_state_by_error (rss->state, + rc); + rss->cb (rss->cb_cls, + ec, + rss->state); + rss->cb = NULL; + free_rss (rss); +} + + +/** + * Determine recovery @a cost of solving a challenge of type @a type + * at @a provider_url by inspecting @a state. + * + * @param state the state to inspect + * @param provider_url the provider to lookup config info from + * @param type the method to lookup the cost of + * @param[out] cost the recovery cost to return + * @return #GNUNET_OK on success, #GNUNET_NO if not found, #GNUNET_SYSERR on state error + */ +static int +lookup_cost (const json_t *state, + const char *provider_url, + const char *type, + struct TALER_Amount *cost) +{ + const json_t *providers; + const json_t *provider; + const json_t *methods; + + providers = json_object_get (state, + "authentication_providers"); + if (NULL == providers) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + provider = json_object_get (providers, + provider_url); + if (NULL == provider) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + methods = json_object_get (provider, + "methods"); + if ( (NULL == methods) || + (! json_is_array (methods)) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + { + size_t index; + json_t *method; + + json_array_foreach (methods, index, method) { + const char *t; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &t), + TALER_JSON_spec_amount_any ("usage_fee", + cost), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (method, + spec, + NULL, NULL)) + { + GNUNET_break (0); + continue; + } + if (0 == strcmp (t, + type)) + return GNUNET_OK; + } + } + return GNUNET_NO; /* not found */ +} + + +/** + * We failed to download a policy. Show an error to the user and + * allow the user to specify alternative providers and/or policy + * versions. + * + * @param[in] rss state to fail with the policy download + * @param offline true of the reason to show is that all providers + * were offline / did not return a salt to us + */ +static void +return_no_policy (struct RecoverSecretState *rss, + bool offline) +{ + json_t *msg; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No provider online, need user to manually specify providers!\n"); + msg = json_pack ("{s:s, s:b}", + "hint", + offline ? "could not contact provider" : + "provider does not know you", + "offline", + offline); + GNUNET_assert (0 == + json_object_set_new (rss->state, + "recovery_error", + msg)); + /* In case there are old ones, remove them! */ + (void) json_object_del (rss->state, + "recovery_document"); + (void) json_object_del (rss->state, + "recovery_information"); + set_state (rss->state, + ANASTASIS_RECOVERY_STATE_SECRET_SELECTING); + rss->cb (rss->cb_cls, + TALER_EC_NONE, + rss->state); + free_rss (rss); +} + + +/** + * Callback which passes back the recovery document and its possible + * policies. Also passes back the version of the document for the user + * to check. + * + * Once the first policy lookup succeeds, we update our state and + * cancel all of the others, passing the obtained recovery information + * back to the user. + * + * @param cls closure for the callback + * @param ri recovery information struct which contains the policies + */ +static void +policy_lookup_cb (void *cls, + const struct ANASTASIS_RecoveryInformation *ri) +{ + struct PolicyDownloadEntry *pd = cls; + struct RecoverSecretState *rss = pd->rss; + json_t *policies; + json_t *challenges; + json_t *recovery_information; + + if (NULL == ri) + { + /* Woopsie, failed hard. */ + GNUNET_CONTAINER_DLL_remove (rss->pd_head, + rss->pd_tail, + pd); + ANASTASIS_recovery_abort (pd->recovery); + GNUNET_free (pd->backend_url); + GNUNET_free (pd); + if (NULL != rss->pd_head) + return; /* wait for another one */ + /* all failed! report failure! */ + return_no_policy (rss, + false); + return; + } + policies = json_array (); + GNUNET_assert (NULL != policies); + for (unsigned int i = 0; i<ri->dps_len; i++) + { + struct ANASTASIS_DecryptionPolicy *dps = ri->dps[i]; + json_t *pchallenges; + + pchallenges = json_array (); + GNUNET_assert (NULL != pchallenges); + for (unsigned int j = 0; j<dps->challenges_length; j++) + { + struct ANASTASIS_Challenge *c = dps->challenges[j]; + const struct ANASTASIS_ChallengeDetails *cd; + json_t *cj; + + cd = ANASTASIS_challenge_get_details (c); + cj = json_pack ("{s:o}", + "uuid", + GNUNET_JSON_from_data_auto (&cd->uuid)); + GNUNET_assert (NULL != cj); + GNUNET_assert (0 == + json_array_append_new (pchallenges, + cj)); + + } + GNUNET_assert (0 == + json_array_append_new (policies, + pchallenges)); + } /* end for all policies */ + challenges = json_array (); + GNUNET_assert (NULL != challenges); + for (unsigned int i = 0; i<ri->cs_len; i++) + { + struct ANASTASIS_Challenge *c = ri->cs[i]; + const struct ANASTASIS_ChallengeDetails *cd; + json_t *cj; + struct TALER_Amount cost; + int ret; + + cd = ANASTASIS_challenge_get_details (c); + ret = lookup_cost (rss->state, + cd->provider_url, + cd->type, + &cost); + if (GNUNET_SYSERR == ret) + { + json_decref (challenges); + json_decref (policies); + set_state (rss->state, + ANASTASIS_GENERIC_STATE_ERROR); + ANASTASIS_redux_fail_ (rss->cb, + rss->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "failed to 'lookup_cost'"); + free_rss (rss); + return; + } + + cj = json_pack ("{s:o,s:o?,s:s,s:s}", + "uuid", + GNUNET_JSON_from_data_auto (&cd->uuid), + "cost", + (GNUNET_NO == ret) + ? NULL + : TALER_JSON_from_amount (&cost), + "type", + cd->type, + "instructions", + cd->instructions); + GNUNET_assert (NULL != cj); + GNUNET_assert (0 == + json_array_append_new (challenges, + cj)); + } /* end for all challenges */ + recovery_information = json_pack ("{s:o, s:o, s:s?, s:s, s:I}", + "challenges", challenges, + "policies", policies, + "secret_name", ri->secret_name, + "provider_url", pd->backend_url, + "version", (json_int_t) ri->version); + GNUNET_assert (NULL != recovery_information); + GNUNET_assert (0 == + json_object_set_new (rss->state, + "recovery_information", + recovery_information)); + { + json_t *rd; + + rd = ANASTASIS_recovery_serialize (pd->recovery); + if (NULL == rd) + { + GNUNET_break (0); + set_state (rss->state, + ANASTASIS_GENERIC_STATE_ERROR); + rss->cb (rss->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + rss->state); + free_rss (rss); + return; + } + GNUNET_assert (0 == + json_object_set_new (rss->state, + "recovery_document", + rd)); + } + /* In case there is an old error remove it! */ + (void) json_object_del (rss->state, + "recovery_error"); + set_state (rss->state, + ANASTASIS_RECOVERY_STATE_SECRET_SELECTING); + rss->cb (rss->cb_cls, + TALER_EC_NONE, + rss->state); + free_rss (rss); +} + + +/** + * Try to launch recovery at provider @a provider_url with config @a p_cfg. + * + * @param[in,out] rss recovery context + * @param provider_url base URL of the provider to try + * @param p_cfg configuration of the provider + * @return true if a recovery was launched + */ +static bool +launch_recovery (struct RecoverSecretState *rss, + const char *provider_url, + const json_t *p_cfg) +{ + struct PolicyDownloadEntry *pd = GNUNET_new (struct PolicyDownloadEntry); + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("salt", + &pd->salt), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (p_cfg, + spec, + NULL, NULL)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No salt for `%s', provider offline?\n", + provider_url); + GNUNET_free (pd); + return false; + } + pd->backend_url = GNUNET_strdup (provider_url); + pd->rss = rss; + pd->recovery = ANASTASIS_recovery_begin (ANASTASIS_REDUX_ctx_, + rss->id_data, + rss->have_version + ? rss->version + : 0, + pd->backend_url, + &pd->salt, + &policy_lookup_cb, + pd, + &core_early_secret_cb, + pd); + if (NULL != pd->recovery) + { + GNUNET_CONTAINER_DLL_insert (rss->pd_head, + rss->pd_tail, + pd); + return true; + } + GNUNET_free (pd->backend_url); + GNUNET_free (pd); + return false; +} + + +/** + * We finished downloading /config from all providers, merge + * into the main state, trigger the continuation and free our + * state. + * + * @param[in] rss main state to merge into + */ +static void +providers_complete (struct RecoverSecretState *rss) +{ + bool launched = false; + struct RecoveryStartStateProviderEntry *pe; + json_t *tlist; + + tlist = json_object_get (rss->state, + "authentication_providers"); + if (NULL == tlist) + { + tlist = json_object (); + GNUNET_assert (NULL != tlist); + GNUNET_assert (0 == + json_object_set_new (rss->state, + "authentication_providers", + tlist)); + } + while (NULL != (pe = rss->pe_head)) + { + json_t *provider_list; + + GNUNET_CONTAINER_DLL_remove (rss->pe_head, + rss->pe_tail, + pe); + provider_list = json_object_get (pe->istate, + "authentication_providers"); + /* merge provider_list into tlist (overriding existing entries) */ + if (NULL != provider_list) + { + const char *url; + json_t *value; + + json_object_foreach (provider_list, url, value) { + GNUNET_assert (0 == + json_object_set (tlist, + url, + value)); + } + } + json_decref (pe->istate); + GNUNET_free (pe); + } + + /* now iterate over providers and begin downloading */ + if (NULL != rss->provider_url) + { + json_t *p_cfg; + + p_cfg = json_object_get (tlist, + rss->provider_url); + if (NULL != p_cfg) + launched = launch_recovery (rss, + rss->provider_url, + p_cfg); + } + else + { + json_t *p_cfg; + const char *provider_url; + + json_object_foreach (tlist, provider_url, p_cfg) + { + launched |= launch_recovery (rss, + provider_url, + p_cfg); + } + } + if (! launched) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No provider online, need user to specify different provider!\n"); + return_no_policy (rss, + true); + return; + } +} + + +/** + * Function called when the complete information about a provider + * was added to @a new_state. + * + * @param cls a `struct RecoveryStartStateProviderEntry` + * @param error error code + * @param new_state resulting new state + */ +static void +provider_added_cb (void *cls, + enum TALER_ErrorCode error, + json_t *new_state) +{ + struct RecoveryStartStateProviderEntry *pe = cls; + + pe->ra = NULL; + pe->istate = json_incref (new_state); + pe->ec = error; + pe->rss->pending--; + if (0 == pe->rss->pending) + providers_complete (pe->rss); +} + + +/** + * Start to query provider for recovery document. + * + * @param[in,out] rss overall recovery state + * @param provider_url base URL of the provider to query + */ +static void +begin_query_provider (struct RecoverSecretState *rss, + const char *provider_url) +{ + struct RecoveryStartStateProviderEntry *pe; + json_t *istate; + + pe = GNUNET_new (struct RecoveryStartStateProviderEntry); + pe->rss = rss; + istate = json_object (); + GNUNET_assert (NULL != istate); + GNUNET_CONTAINER_DLL_insert (rss->pe_head, + rss->pe_tail, + pe); + pe->ra = ANASTASIS_REDUX_add_provider_to_state_ (provider_url, + istate, + &provider_added_cb, + pe); + json_decref (istate); + if (NULL != pe->ra) + rss->pending++; +} + + +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_recovery_challenge_begin_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *version; + json_t *providers; + const json_t *attributes; + struct RecoverSecretState *rss; + const char *provider_url; + + providers = json_object_get (state, + "authentication_providers"); + if ( (NULL == providers) || + (! json_is_object (providers)) ) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_providers' missing"); + return NULL; + } + attributes = json_object_get (arguments, + "identity_attributes"); + if ( (NULL == attributes) || + (! json_is_object (attributes)) ) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'identity_attributes' missing"); + return NULL; + } + rss = GNUNET_new (struct RecoverSecretState); + rss->id_data = json_incref ((json_t *) attributes); + version = json_object_get (arguments, + "version"); + if (NULL != version) + { + rss->version = (unsigned int) json_integer_value (version); + rss->have_version = true; + } + rss->state = json_incref (state); + rss->cb = cb; + rss->cb_cls = cb_cls; + rss->pending = 1; /* decremented after initialization loop */ + + provider_url = json_string_value (json_object_get (arguments, + "provider_url")); + if (NULL != provider_url) + { + rss->provider_url = GNUNET_strdup (provider_url); + begin_query_provider (rss, + provider_url); + } + else + { + json_t *prov; + const char *url; + + json_object_foreach (providers, url, prov) { + begin_query_provider (rss, + url); + } + } + rss->pending--; + if (0 == rss->pending) + { + providers_complete (rss); + if (NULL == rss->cb) + return NULL; + } + rss->ra.cleanup = &free_rss; + rss->ra.cleanup_cls = rss; + return &rss->ra; +} diff --git a/src/reducer/anastasis_api_redux.c b/src/reducer/anastasis_api_redux.c new file mode 100644 index 0000000..eb7e362 --- /dev/null +++ b/src/reducer/anastasis_api_redux.c @@ -0,0 +1,1730 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/anastasis_api_redux.c + * @brief anastasis reducer api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include <platform.h> +#include <jansson.h> +#include "anastasis_redux.h" +#include "anastasis_error_codes.h" +#include <taler/taler_json_lib.h> +#include "anastasis_api_redux.h" +#include <dlfcn.h> + + +/** + * How long do we wait at most for a /config reply from an Anastasis provider. + * 60s is very generous, given the tiny bandwidth required, even for the most + * remote locations. + */ +#define CONFIG_GENERIC_TIMEOUT GNUNET_TIME_UNIT_MINUTES + + +#define GENERATE_STRING(STRING) #STRING, +static const char *generic_strings[] = { + ANASTASIS_GENERIC_STATES (GENERATE_STRING) +}; +#undef GENERATE_STRING + + +/** + * #ANASTASIS_REDUX_add_provider_to_state_ waiting for the + * configuration request to complete or fail. + */ +struct ConfigReduxWaiting +{ + /** + * Kept in a DLL. + */ + struct ConfigReduxWaiting *prev; + + /** + * Kept in a DLL. + */ + struct ConfigReduxWaiting *next; + + /** + * Associated redux action. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * Config request we are waiting for. + */ + struct ConfigRequest *cr; + + /** + * State we are processing. + */ + json_t *state; + + /** + * Function to call with updated @e state. + */ + ANASTASIS_ActionCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + +}; + + +/** + * Anastasis authorization method configuration + */ +struct AuthorizationMethodConfig +{ + /** + * Type of the method, i.e. "question". + */ + char *type; + + /** + * Fee charged for accessing key share using this method. + */ + struct TALER_Amount usage_fee; +}; + + +/** + * State for a "get config" operation. + */ +struct ConfigRequest +{ + + /** + * Kept in a DLL, given that we may have multiple backends. + */ + struct ConfigRequest *next; + + /** + * Kept in a DLL, given that we may have multiple backends. + */ + struct ConfigRequest *prev; + + /** + * Head of DLL of REDUX operations waiting for an answer. + */ + struct ConfigReduxWaiting *w_head; + + /** + * Tail of DLL of REDUX operations waiting for an answer. + */ + struct ConfigReduxWaiting *w_tail; + + /** + * Obtained status code. + */ + unsigned int http_status; + + /** + * The /config GET operation handle. + */ + struct ANASTASIS_ConfigOperation *co; + + /** + * URL of the anastasis backend. + */ + char *url; + + /** + * Business name of the anastasis backend. + */ + char *business_name; + + /** + * currency used by the anastasis backend. + */ + char *currency; + + /** + * Array of authorization methods supported by the server. + */ + struct AuthorizationMethodConfig *methods; + + /** + * Length of the @e methods array. + */ + unsigned int methods_length; + + /** + * Maximum size of an upload in megabytes. + */ + uint32_t storage_limit_in_megabytes; + + /** + * Annual fee for an account / policy upload. + */ + struct TALER_Amount annual_fee; + + /** + * Fee for a truth upload. + */ + struct TALER_Amount truth_upload_fee; + + /** + * Maximum legal liability for data loss covered by the + * provider. + */ + struct TALER_Amount liability_limit; + + /** + * Server salt. + */ + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + + /** + * Task to timeout /config requests. + */ + struct GNUNET_SCHEDULER_Task *tt; + + /** + * Status of the /config request. + */ + enum TALER_ErrorCode ec; +}; + + +/** + * Reducer API's CURL context handle. + */ +struct GNUNET_CURL_Context *ANASTASIS_REDUX_ctx_; + +/** + * JSON containing country specific identity attributes to ask the user for. + */ +static json_t *redux_id_attr; + +/** + * Head of DLL of Anastasis backend configuration requests. + */ +static struct ConfigRequest *cr_head; + +/** + * Tail of DLL of Anastasis backend configuration requests. + */ +static struct ConfigRequest *cr_tail; + +/** + * JSON containing country specific information. + */ +static json_t *redux_countries; + +/** + * List of Anastasis providers. + */ +static json_t *provider_list; + + +/** + * Extract the mode of a state from json + * + * @param state the state to operate on + * @return "backup_state" or "recovery_state" + */ +static const char * +get_state_mode (const json_t *state) +{ + if (json_object_get (state, "backup_state")) + return "backup_state"; + if (json_object_get (state, "recovery_state")) + return "recovery_state"; + GNUNET_assert (0); + return NULL; +} + + +enum ANASTASIS_GenericState +ANASTASIS_generic_state_from_string_ (const char *state_string) +{ + for (enum ANASTASIS_GenericState i = 0; + i < sizeof (generic_strings) / sizeof(*generic_strings); + i++) + if (0 == strcmp (state_string, + generic_strings[i])) + return i; + return ANASTASIS_GENERIC_STATE_ERROR; +} + + +const char * +ANASTASIS_generic_state_to_string_ (enum ANASTASIS_GenericState gs) +{ + if ( (gs < 0) || + (gs >= sizeof (generic_strings) / sizeof(*generic_strings)) ) + { + GNUNET_break_op (0); + return NULL; + } + return generic_strings[gs]; +} + + +void +ANASTASIS_redux_fail_ (ANASTASIS_ActionCallback cb, + void *cb_cls, + enum TALER_ErrorCode ec, + const char *detail) +{ + json_t *estate; + + estate = json_pack ("{s:s?, s:I, s:s}", + "detail", detail, + "code", (json_int_t) ec, + "hint", TALER_ErrorCode_get_hint (ec)); + cb (cb_cls, + ec, + estate); + json_decref (estate); +} + + +/** + * Transition the @a state to @a gs. + * + * @param[in,out] state to transition + * @param gs state to transition to + */ +static void +redux_transition (json_t *state, + enum ANASTASIS_GenericState gs) +{ + const char *s_mode = get_state_mode (state); + + GNUNET_assert (0 == + json_object_set_new ( + state, + s_mode, + json_string ( + ANASTASIS_generic_state_to_string_ (gs)))); + +} + + +void +ANASTASIS_redux_init (struct GNUNET_CURL_Context *ctx) +{ + ANASTASIS_REDUX_ctx_ = ctx; +} + + +/** + * Function to free a #ConfigRequest, an async operation. + * + * @param cr state for a "get config" operation + */ +static void +free_config_request (struct ConfigRequest *cr) +{ + GNUNET_assert (NULL == cr->w_head); + if (NULL != cr->co) + ANASTASIS_config_cancel (cr->co); + if (NULL != cr->tt) + GNUNET_SCHEDULER_cancel (cr->tt); + GNUNET_free (cr->currency); + GNUNET_free (cr->url); + GNUNET_free (cr->business_name); + for (unsigned int i = 0; i<cr->methods_length; i++) + GNUNET_free (cr->methods[i].type); + GNUNET_free (cr->methods); + GNUNET_free (cr); +} + + +void +ANASTASIS_redux_done () +{ + struct ConfigRequest *cr; + + while (NULL != (cr = cr_head)) + { + GNUNET_CONTAINER_DLL_remove (cr_head, + cr_tail, + cr); + free_config_request (cr); + } + ANASTASIS_REDUX_ctx_ = NULL; + if (NULL != redux_countries) + { + json_decref (redux_countries); + redux_countries = NULL; + } + if (NULL != redux_id_attr) + { + json_decref (redux_id_attr); + redux_id_attr = NULL; + } + if (NULL != provider_list) + { + json_decref (provider_list); + provider_list = NULL; + } +} + + +const json_t * +ANASTASIS_redux_countries_init_ (void) +{ + char *dn; + json_error_t error; + + if (NULL != redux_countries) + return redux_countries; + + { + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return NULL; + } + GNUNET_asprintf (&dn, + "%s/redux.countries.json", + path); + GNUNET_free (path); + } + redux_countries = json_load_file (dn, + JSON_COMPACT, + &error); + if (NULL == redux_countries) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse `%s': %s at %d:%d (%d)\n", + dn, + error.text, + error.line, + error.column, + error.position); + GNUNET_free (dn); + return NULL; + } + GNUNET_free (dn); + return redux_countries; +} + + +/** + * Abort waiting for /config reply. + * + * @param cls a `struct ConfigReduxWaiting` handle. + */ +static void +abort_provider_config_cb (void *cls) +{ + struct ConfigReduxWaiting *w = cls; + struct ConfigRequest *cr = w->cr; + + GNUNET_CONTAINER_DLL_remove (cr->w_head, + cr->w_tail, + w); + json_decref (w->state); + GNUNET_free (w); +} + + +/** + * Notify anyone waiting on @a cr that the request is done + * (successful or failed). + * + * @param[in,out] cr request that completed + */ +static void +notify_waiting (struct ConfigRequest *cr) +{ + struct ConfigReduxWaiting *w; + + while (NULL != (w = cr->w_head)) + { + json_t *provider_list; + json_t *prov; + + if (NULL == (provider_list = json_object_get (w->state, + "authentication_providers"))) + { + GNUNET_assert (0 == + json_object_set_new (w->state, + "authentication_providers", + provider_list = json_object ())); + } + provider_list = json_object_get (w->state, + "authentication_providers"); + GNUNET_assert (NULL != provider_list); + + if (TALER_EC_NONE != cr->ec) + { + prov = json_pack ("{s:I, s:I}", + "error_code", + (json_int_t) cr->ec, + "http_status", + (json_int_t) cr->http_status); + } + else + { + json_t *methods_list; + + methods_list = json_array (); + GNUNET_assert (NULL != methods_list); + for (unsigned int i = 0; i<cr->methods_length; i++) + { + struct AuthorizationMethodConfig *method = &cr->methods[i]; + json_t *mj = json_pack ("{s:s, s:o}", + "type", + method->type, + "usage_fee", + TALER_JSON_from_amount (&method->usage_fee)); + + GNUNET_assert (NULL != mj); + GNUNET_assert (0 == + json_array_append_new (methods_list, + mj)); + } + prov = json_pack ("{s:o, s:o, s:o, s:o, s:s," + " s:s, s:I, s:o, s:I}", + "methods", + methods_list, + "annual_fee", + TALER_JSON_from_amount (&cr->annual_fee), + "truth_upload_fee", + TALER_JSON_from_amount (&cr->truth_upload_fee), + "liability_limit", + TALER_JSON_from_amount (&cr->liability_limit), + "currency", + cr->currency, + /* 6 */ + "business_name", + cr->business_name, + "storage_limit_in_megabytes", + (json_int_t) cr->storage_limit_in_megabytes, + "salt", + GNUNET_JSON_from_data_auto (&cr->salt), + "http_status", + (json_int_t) cr->http_status); + } + GNUNET_assert (0 == + json_object_set_new (provider_list, + cr->url, + prov)); + w->cb (w->cb_cls, + cr->ec, + w->state); + abort_provider_config_cb (w); + } + +} + + +/** + * Function called with the results of a #ANASTASIS_get_config(). + * + * @param cls closure + * @param http_status HTTP status of the request + * @param acfg anastasis configuration + */ +static void +config_cb (void *cls, + unsigned int http_status, + const struct ANASTASIS_Config *acfg) +{ + struct ConfigRequest *cr = cls; + + cr->co = NULL; + GNUNET_SCHEDULER_cancel (cr->tt); + cr->tt = NULL; + cr->http_status = http_status; + if (MHD_HTTP_OK != http_status) + cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED; + if ( (MHD_HTTP_OK == http_status) && + (NULL == acfg) ) + { + cr->http_status = MHD_HTTP_NOT_FOUND; + cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED; + } + else if (NULL != acfg) + { + if (0 == acfg->storage_limit_in_megabytes) + { + cr->http_status = 0; + cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG; + } + else + { + cr->currency = GNUNET_strdup (acfg->currency); + cr->business_name = GNUNET_strdup (acfg->business_name); + cr->methods = GNUNET_new_array (acfg->methods_length, + struct AuthorizationMethodConfig); + for (unsigned int i = 0; i<acfg->methods_length; i++) + { + cr->methods[i].type = GNUNET_strdup (acfg->methods[i].type); + cr->methods[i].usage_fee = acfg->methods[i].usage_fee; + } + cr->methods_length = acfg->methods_length; + cr->storage_limit_in_megabytes = acfg->storage_limit_in_megabytes; + cr->annual_fee = acfg->annual_fee; + cr->truth_upload_fee = acfg->truth_upload_fee; + cr->liability_limit = acfg->liability_limit; + cr->salt = acfg->salt; + } + } + notify_waiting (cr); +} + + +/** + * Aborts a "get config" after timeout. + * + * @param cls closure for a "get config" request + */ +static void +config_request_timeout (void *cls) +{ + struct ConfigRequest *cr = cls; + + cr->tt = NULL; + ANASTASIS_config_cancel (cr->co); + cr->co = NULL; + cr->http_status = 0; + cr->ec = TALER_EC_GENERIC_TIMEOUT; + notify_waiting (cr); +} + + +/** + * Schedule job to obtain Anastasis provider configuration at @a url. + * + * @param url base URL of Anastasis provider + * @return check config handle + */ +static struct ConfigRequest * +check_config (const char *url) +{ + struct ConfigRequest *cr; + + for (cr = cr_head; NULL != cr; cr = cr->next) + { + if (0 != strcmp (url, + cr->url)) + continue; + if (NULL != cr->co) + return cr; /* already on it */ + break; + } + if (NULL == cr) + { + cr = GNUNET_new (struct ConfigRequest); + cr->url = GNUNET_strdup (url); + GNUNET_CONTAINER_DLL_insert (cr_head, + cr_tail, + cr); + } + cr->co = ANASTASIS_get_config (ANASTASIS_REDUX_ctx_, + cr->url, + &config_cb, + cr); + if (NULL == cr->co) + { + GNUNET_break (0); + return NULL; + } + else + { + cr->tt = GNUNET_SCHEDULER_add_delayed (CONFIG_GENERIC_TIMEOUT, + &config_request_timeout, + cr); + } + return cr; +} + + +/** + * Begin asynchronous check for provider configurations. + * + * @param currencies the currencies to initiate the provider checks for + * @param[in,out] state to set provider list for + * @return #TALER_EC_NONE on success + */ +static enum TALER_ErrorCode +begin_provider_config_check (const json_t *currencies, + json_t *state) +{ + if (NULL == provider_list) + { + json_error_t error; + char *dn; + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + GNUNET_asprintf (&dn, + "%s/provider-list.json", + path); + GNUNET_free (path); + provider_list = json_load_file (dn, + JSON_COMPACT, + &error); + if (NULL == provider_list) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse `%s': %s at %d:%d (%d)\n", + dn, + error.text, + error.line, + error.column, + error.position); + GNUNET_free (dn); + return TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED; + } + GNUNET_free (dn); + } + + { + size_t index; + json_t *provider; + const json_t *provider_arr = json_object_get (provider_list, + "anastasis_provider"); + json_t *pl; + + pl = json_object (); + json_array_foreach (provider_arr, index, provider) + { + const char *url; + const char *cur; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("url", + &url), + GNUNET_JSON_spec_string ("currency", + &cur), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (provider, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (pl); + return TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED; + } + + { + bool found = false; + json_t *cu; + size_t off; + + json_array_foreach (currencies, off, cu) + { + const char *currency; + + currency = json_string_value (cu); + if (NULL == currency) + { + json_decref (pl); + return TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID; + } + found = (0 == strcasecmp (currency, + cur)); + } + if (! found) + continue; + } + GNUNET_assert (0 == + json_object_set_new (pl, + url, + json_object ())); + check_config (url); + } + GNUNET_assert (0 == + json_object_set_new (state, + "authentication_providers", + pl)); + } + return TALER_EC_NONE; +} + + +/** + * Function to validate an input by regular expression ("validation-regex"). + * + * @param input text to validate + * @param regexp regular expression to validate + * @return true if validation passed, else false + */ +static bool +validate_regex (const char *input, + const char *regexp) +{ + regex_t regex; + + if (0 != regcomp (®ex, + regexp, + REG_EXTENDED)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to compile regular expression `%s'.", + regexp); + return true; + } + /* check if input has correct form */ + if (0 != regexec (®ex, + input, + 0, + NULL, + 0)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Input `%s' does not match regex `%s'\n", + input, + regexp); + regfree (®ex); + return false; + } + regfree (®ex); + return true; +} + + +/** + * Function to load json containing country specific identity + * attributes. Uses a single-slot cache to avoid loading + * exactly the same attributes twice. + * + * @param country_code country code (e.g. "de") + * @return NULL on error + */ +static const json_t * +redux_id_attr_init (const char *country_code) +{ + static char redux_id_cc[3]; + char *dn; + json_error_t error; + + if (0 == strcmp (country_code, + redux_id_cc)) + return redux_id_attr; + + if (NULL != redux_id_attr) + { + json_decref (redux_id_attr); + redux_id_attr = NULL; + } + { + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return NULL; + } + GNUNET_asprintf (&dn, + "%s/redux.%s.json", + path, + country_code); + GNUNET_free (path); + } + redux_id_attr = json_load_file (dn, + JSON_COMPACT, + &error); + if (NULL == redux_id_attr) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse `%s': %s at %d:%d (%d)\n", + dn, + error.text, + error.line, + error.column, + error.position); + GNUNET_free (dn); + return NULL; + } + GNUNET_free (dn); + strncpy (redux_id_cc, + country_code, + sizeof (redux_id_cc)); + redux_id_cc[2] = '\0'; + return redux_id_attr; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "select_continent" 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 * +select_continent (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *redux_countries = ANASTASIS_redux_countries_init_ (); + const json_t *root = json_object_get (redux_countries, + "countries"); + const json_t *continent; + json_t *countries; + + if (NULL == root) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED, + "'countries' missing"); + return NULL; + } + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + continent = json_object_get (arguments, + "continent"); + if (NULL == continent) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'continent' missing"); + return NULL; + } + countries = json_array (); + GNUNET_assert (NULL != countries); + { + size_t index; + const json_t *country; + bool found = false; + + json_array_foreach (root, index, country) + { + json_t *temp_continent = json_object_get (country, + "continent"); + if (1 == json_equal (continent, + temp_continent)) + { + GNUNET_assert (0 == + json_array_append (countries, + (json_t *) country)); + found = true; + } + } + if (! found) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'continent' unknown"); + return NULL; + } + } + redux_transition (state, + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING); + GNUNET_assert (0 == + json_object_set (state, + "selected_continent", + (json_t *) continent)); + GNUNET_assert (0 == + json_object_set_new (state, + "countries", + countries)); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "select_country" 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 #ANASTASIS_ReduxAction + */ +static struct ANASTASIS_ReduxAction * +select_country (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *required_attrs; + const json_t *country_code; + const json_t *currencies; + const json_t *redux_id_attr; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + country_code = json_object_get (arguments, + "country_code"); + if (NULL == country_code) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'country_code' missing"); + return NULL; + } + + { + json_t *countries = json_object_get (state, + "countries"); + size_t index; + json_t *country; + bool found = false; + + json_array_foreach (countries, index, country) + { + json_t *cc = json_object_get (country, + "code"); + if (1 == json_equal (country_code, + cc)) + { + found = true; + break; + } + } + if (! found) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "specified country not on selected continent"); + return NULL; + } + } + + currencies = json_object_get (arguments, + "currencies"); + if ( (NULL == currencies) || + (! json_is_array (currencies)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'currencies' missing"); + return NULL; + } + /* We now have an idea of the currency, begin fetching + provider /configs (we likely need them later) */ + { + enum TALER_ErrorCode ec; + + ec = begin_provider_config_check (currencies, + state); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + ec, + NULL); + return NULL; + } + } + redux_id_attr = redux_id_attr_init (json_string_value (country_code)); + if (NULL == redux_id_attr) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_RESOURCE_MISSING, + json_string_value (country_code)); + return NULL; + } + required_attrs = json_object_get (redux_id_attr, + "required_attributes"); + if (NULL == required_attrs) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED, + "'required_attributes' missing"); + return NULL; + } + redux_transition (state, + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING); + GNUNET_assert (0 == + json_object_set (state, + "selected_country", + (json_t *) country_code)); + GNUNET_assert (0 == + json_object_set (state, + "currencies", + (json_t *) currencies)); + GNUNET_assert (0 == + json_object_set (state, + "required_attributes", + (json_t *) required_attrs)); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "unselect_continent" 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 * +unselect_continent (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + redux_transition (state, + ANASTASIS_GENERIC_STATE_CONTINENT_SELECTING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_add_provider_to_state_ (const char *url, + json_t *state, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct ConfigRequest *cr; + struct ConfigReduxWaiting *w; + + cr = check_config (url); + w = GNUNET_new (struct ConfigReduxWaiting); + w->cr = cr; + w->state = json_incref (state); + w->cb = cb; + w->cb_cls = cb_cls; + w->ra.cleanup = &abort_provider_config_cb; + w->ra.cleanup_cls = w; + GNUNET_CONTAINER_DLL_insert (cr->w_head, + cr->w_tail, + w); + if (NULL == cr->co) + { + notify_waiting (cr); + return NULL; + } + return &w->ra; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "enter_user_attributes" 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 * +enter_user_attributes (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *attributes; + const json_t *required_attributes; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + attributes = json_object_get (arguments, + "identity_attributes"); + if (NULL == attributes) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'identity_attributes' missing"); + return NULL; + } + GNUNET_assert (0 == + json_object_set (state, + "identity_attributes", + (json_t *) attributes)); + + /* Verify required attributes are present and well-formed */ + required_attributes = json_object_get (state, + "required_attributes"); + if ( (NULL == required_attributes) || + (! json_is_array (required_attributes)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'required_attributes' must be an array"); + return NULL; + } + { + size_t index; + json_t *required_attribute; + + json_array_foreach (required_attributes, index, required_attribute) + { + const char *name; + const char *attribute_value; + const char *regexp = NULL; + const char *reglog = NULL; + int optional = false; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &name), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("validation-regex", + ®exp)), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("validation-logic", + ®log)), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_boolean ("optional", + &optional)), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (required_attribute, + spec, + NULL, NULL)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'required_attributes' lacks required fields"); + return NULL; + } + attribute_value = json_string_value (json_object_get (attributes, + name)); + if (NULL == attribute_value) + { + if (optional) + continue; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Request is missing required attribute `%s'\n", + name); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_GENERIC_PARAMETER_MISSING, + name); + return NULL; + } + if ( (NULL != regexp) && + (! validate_regex (attribute_value, + regexp)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_REGEX_FAILED, + name); + return NULL; + } + + if (NULL != reglog) + { + bool (*regfun)(const char *); + + regfun = dlsym (RTLD_DEFAULT, + reglog); + if (NULL == regfun) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Custom validation function `%s' is not available: %s\n", + reglog, + dlerror ()); + } + else if (! regfun (attribute_value)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED, + name); + return NULL; + } + } + } /* end for all attributes loop */ + } /* end for all attributes scope */ + + /* Transition based on mode */ + { + const char *s_mode = get_state_mode (state); + + if (0 == strcmp (s_mode, + "backup_state")) + { + GNUNET_assert (0 == + json_object_set_new ( + state, + "backup_state", + json_string ( + ANASTASIS_backup_state_to_string_ ( + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING)))); + return ANASTASIS_REDUX_backup_begin_ (state, + arguments, + cb, + cb_cls); + } + else + { + GNUNET_assert (0 == + json_object_set_new ( + state, + "recovery_state", + json_string ( + ANASTASIS_recovery_state_to_string_ ( + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING)))); + return ANASTASIS_REDUX_recovery_challenge_begin_ (state, + arguments, + cb, + cb_cls); + } + } +} + + +/** + * 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; + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +bool +ANASTASIS_add_provider_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *urls; + json_t *tlist; + + tlist = json_object_get (state, + "authentication_providers"); + if (NULL == tlist) + { + tlist = json_object (); + GNUNET_assert (NULL != tlist); + GNUNET_assert (0 == + json_object_set_new (state, + "authentication_providers", + tlist)); + } + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return true; + } + urls = json_object_get (arguments, + "urls"); + if (NULL == urls) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'urls' missing"); + return true; + } + { + size_t index; + json_t *url; + + json_array_foreach (urls, index, url) + { + const char *url_str = json_string_value (url); + + if (NULL == url_str) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'urls' must be strings"); + return true; + } + GNUNET_assert (0 == + json_object_set_new (tlist, + url_str, + json_object ())); + } + } + return false; +} + + +struct ANASTASIS_ReduxAction * +ANASTASIS_back_generic_decrement_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const char *s_mode = get_state_mode (state); + const char *state_string = json_string_value (json_object_get (state, + s_mode)); + + (void) arguments; + GNUNET_assert (NULL != state_string); + if (0 == strcmp ("backup_state", + s_mode)) + { + enum ANASTASIS_BackupState state_index; + + state_index = ANASTASIS_backup_state_from_string_ (state_string); + GNUNET_assert (state_index > 0); + state_index = state_index - 1; + + GNUNET_assert (0 == + json_object_set_new ( + state, + s_mode, + json_string ( + ANASTASIS_backup_state_to_string_ (state_index)))); + } + else + { + enum ANASTASIS_RecoveryState state_index; + + state_index = ANASTASIS_recovery_state_from_string_ (state_string); + GNUNET_assert (state_index > 0); + state_index = state_index - 1; + GNUNET_assert (0 == + json_object_set_new ( + state, + s_mode, + json_string ( + ANASTASIS_recovery_state_to_string_ (state_index)))); + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * Callback function which is called by the reducer in dependence of + * given state and action. + * + * @param state the previous state to operate on + * @param arguments the arguments needed by operation to operate on state + * @param cb Callback function which returns the new state + * @param cb_cls closure for @a cb + * @return handle to cancel async actions, NULL if @a cb was already called + */ +typedef struct ANASTASIS_ReduxAction * +(*DispatchHandler)(json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +struct ANASTASIS_ReduxAction * +ANASTASIS_redux_action (const json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct Dispatcher + { + enum ANASTASIS_GenericState redux_state; + const char *redux_action; + DispatchHandler fun; + } dispatchers[] = { + { + ANASTASIS_GENERIC_STATE_CONTINENT_SELECTING, + "select_continent", + &select_continent + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "unselect_continent", + &unselect_continent + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "select_country", + &select_country + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "select_continent", + &select_continent + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "unselect_continent", + &unselect_continent + }, + { + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING, + "enter_user_attributes", + &enter_user_attributes + }, + { + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING, + "add_provider", + &add_provider + }, + { + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { ANASTASIS_GENERIC_STATE_ERROR, NULL, NULL } + }; + bool recovery_mode = false; + const char *s = json_string_value (json_object_get (state, + "backup_state")); + enum ANASTASIS_GenericState gs; + + if (NULL == s) + { + s = json_string_value (json_object_get (state, + "recovery_state")); + if (NULL == s) + { + GNUNET_break_op (0); + cb (cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + NULL); + return NULL; + } + recovery_mode = true; + } + gs = ANASTASIS_generic_state_from_string_ (s); + { + json_t *new_state; + struct ANASTASIS_ReduxAction *ret; + + new_state = json_deep_copy (state); + GNUNET_assert (NULL != new_state); + if (gs != ANASTASIS_GENERIC_STATE_ERROR) + { + for (unsigned int i = 0; NULL != dispatchers[i].fun; i++) + { + if ( (gs == dispatchers[i].redux_state) && + (0 == strcmp (action, + dispatchers[i].redux_action)) ) + { + ret = dispatchers[i].fun (new_state, + arguments, + cb, + cb_cls); + json_decref (new_state); + return ret; + } + } + } + if (recovery_mode) + { + ret = ANASTASIS_recovery_action_ (new_state, + action, + arguments, + cb, + cb_cls); + } + else + { + ret = ANASTASIS_backup_action_ (new_state, + action, + arguments, + cb, + cb_cls); + } + json_decref (new_state); + return ret; + } +} + + +void +ANASTASIS_redux_action_cancel (struct ANASTASIS_ReduxAction *ra) +{ + ra->cleanup (ra->cleanup_cls); +} + + +json_t * +ANASTASIS_REDUX_load_continents_ () +{ + const json_t *countries; + json_t *continents; + const json_t *redux_countries = ANASTASIS_redux_countries_init_ (); + + if (NULL == redux_countries) + { + GNUNET_break (0); + return NULL; + } + countries = json_object_get (redux_countries, + "countries"); + if (NULL == countries) + { + GNUNET_break (0); + return NULL; + } + continents = json_array (); + GNUNET_assert (NULL != continents); + + { + json_t *country; + size_t index; + + json_array_foreach (countries, index, country) + { + json_t *ex = NULL; + const json_t *continent; + + continent = json_object_get (country, + "continent"); + if ( (NULL == continent) || + (! json_is_string (continent)) ) + { + GNUNET_break (0); + continue; + } + { + size_t inner_index; + json_t *inner_continent; + + json_array_foreach (continents, inner_index, inner_continent) + { + const json_t *name; + + name = json_object_get (inner_continent, + "name"); + if (1 == json_equal (continent, + name)) + { + ex = inner_continent; + break; + } + } + } + if (NULL == ex) + { + ex = json_pack ("{s:O}", + "name", + continent); + GNUNET_assert (0 == + json_array_append_new (continents, + ex)); + } + + { + json_t *i18n_continent; + json_t *name_ex; + + i18n_continent = json_object_get (country, + "continent_i18n"); + name_ex = json_object_get (ex, + "name_i18n"); + if (NULL != i18n_continent) + { + const char *lang; + json_t *trans; + + json_object_foreach (i18n_continent, lang, trans) + { + if (NULL == name_ex) + { + name_ex = json_object (); + json_object_set_new (ex, + "name_i18n", + name_ex); + } + if (NULL == json_object_get (name_ex, + lang)) + { + json_object_set (name_ex, + lang, + trans); + } + } + } + } + } + } + return json_pack ("{s:o}", + "continents", + continents); +} diff --git a/src/reducer/anastasis_api_redux.h b/src/reducer/anastasis_api_redux.h new file mode 100644 index 0000000..19d9466 --- /dev/null +++ b/src/reducer/anastasis_api_redux.h @@ -0,0 +1,347 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file redux/anastasis_api_redux.h + * @brief anastasis reducer api, internal data structures + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#ifndef ANASTASIS_API_REDUX_H +#define ANASTASIS_API_REDUX_H + + +#define ANASTASIS_GENERIC_STATES(REDUX_STATE) \ + REDUX_STATE (ERROR) \ + REDUX_STATE (CONTINENT_SELECTING) \ + REDUX_STATE (COUNTRY_SELECTING) \ + REDUX_STATE (USER_ATTRIBUTES_COLLECTING) + +#define GENERATE_GENERIC_ENUM(ENUM) ANASTASIS_GENERIC_STATE_ ## ENUM, + +enum ANASTASIS_GenericState +{ + ANASTASIS_GENERIC_STATES (GENERATE_GENERIC_ENUM) +}; + +#undef GENERATE_GENERIC_ENUM + +#define ANASTASIS_BACKUP_STATES(REDUX_STATE) \ + ANASTASIS_GENERIC_STATES (REDUX_STATE) \ + REDUX_STATE (AUTHENTICATIONS_EDITING) \ + REDUX_STATE (POLICIES_REVIEWING) \ + REDUX_STATE (SECRET_EDITING) \ + REDUX_STATE (TRUTHS_PAYING) \ + REDUX_STATE (POLICIES_PAYING) \ + REDUX_STATE (BACKUP_FINISHED) + +#define GENERATE_BACKUP_ENUM(ENUM) ANASTASIS_BACKUP_STATE_ ## ENUM, + +enum ANASTASIS_BackupState +{ + ANASTASIS_BACKUP_STATES (GENERATE_BACKUP_ENUM) +}; + +#undef GENERATE_BACKUP_ENUM + +#define ANASTASIS_RECOVERY_STATES(REDUX_STATE) \ + ANASTASIS_GENERIC_STATES (REDUX_STATE) \ + REDUX_STATE (SECRET_SELECTING) \ + REDUX_STATE (CHALLENGE_SELECTING) \ + REDUX_STATE (CHALLENGE_PAYING) \ + REDUX_STATE (CHALLENGE_SOLVING) \ + REDUX_STATE (RECOVERY_FINISHED) + +#define GENERATE_RECOVERY_ENUM(ENUM) ANASTASIS_RECOVERY_STATE_ ## ENUM, + +enum ANASTASIS_RecoveryState +{ + ANASTASIS_RECOVERY_STATES (GENERATE_RECOVERY_ENUM) +}; + +#undef GENERATE_RECOVERY_ENUM + + +/** + * CURL context to be used by all operations. + */ +extern struct GNUNET_CURL_Context *ANASTASIS_REDUX_ctx_; + + +/** + * Initialize reducer subsystem. + * + * @param ctx context to use for CURL requests. + */ +void +ANASTASIS_redux_init (struct GNUNET_CURL_Context *ctx); + + +/** + * Terminate reducer subsystem. + */ +void +ANASTASIS_redux_done (void); + + +/** + * Produce an initial state with an initialized list of + * continents. + */ +json_t * +ANASTASIS_REDUX_load_continents_ (void); + + +/** + * Returns the enum value to a string value of a state. + * + * @param state_string + * @return #ANASTASIS_GENERIC_STATE_ERROR on error + */ +enum ANASTASIS_GenericState +ANASTASIS_generic_state_from_string_ (const char *state_string); + + +/** + * Returns the string value of a state. + * + * @param state_string + * @return NULL on error + */ +const char * +ANASTASIS_generic_state_to_string_ (enum ANASTASIS_GenericState gs); + + +/** + * Returns the enum value to a string value of a state. + * + * @param state_string + * @return #ANASTASIS_BACKUP_STATE_ERROR on error + */ +enum ANASTASIS_BackupState +ANASTASIS_backup_state_from_string_ (const char *state_string); + + +/** + * Returns the string value of a state. + * + * @param state_string + * @return NULL on error + */ +const char * +ANASTASIS_backup_state_to_string_ (enum ANASTASIS_BackupState bs); + + +/** + * Returns the enum value to a string value of a state. + * + * @param state_string + * @return XXX on error + */ +enum ANASTASIS_RecoveryState +ANASTASIS_recovery_state_from_string_ (const char *state_string); + + +/** + * Returns the string value of a state. + * + * @param state_string + * @return NULL on error + */ +const char * +ANASTASIS_recovery_state_to_string_ (enum ANASTASIS_RecoveryState rs); + + +/** + * Function to return a json error response. + * + * @param cb callback to give error to + * @param cb_cls callback closure + * @param ec error code + * @param detail error detail + */ +void +ANASTASIS_redux_fail_ (ANASTASIS_ActionCallback cb, + void *cb_cls, + enum TALER_ErrorCode ec, + const char *detail); + + +/** + * 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 + * @return true if @a cb was invoked + */ +bool +ANASTASIS_add_provider_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * Adds the server configuration of the Anastasis provider + * at @a url to the json @a state. Checks if we have + * the provider information already available. If so, + * imports it into @a state. If not, queries the provider, + * generating a success or failure outcome asynchronously. + * + * @param cr the config request + * @param[in,out] state the json state to operate on + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return handle to cancel asynchronous operation, NULL if + * we completed synchronously + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_add_provider_to_state_ (const char *url, + json_t *state, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * A generic DispatchHandler/Callback function which is called for a + * "back" 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 (no asynchronous action) + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_back_generic_decrement_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * Function to load json containing all countries. + * Returns the countries. + * + * @return json_t * + */ +const json_t * +ANASTASIS_redux_countries_init_ (void); + + +/** + * Operates on a recovery state depending on given #ANASTASIS_RecoveryState + * and #ANASTASIS_RecoveryAction. The new #ANASTASIS_RecoveryState is returned + * by a callback function. + * This function can do network access to talk to anastasis service providers. + * + * @param ctx the CURL context used to connect to the backend + * @param[in,out] state input/output state (to be modified) + * @param action what action to perform + * @param arguments data for the @a action + * @param cb function to call with the result + * @param cb_cls closure for @a cb + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_recovery_action_ (json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * DispatchHandler/Callback function which is called for a + * "enter_user_attributes" action after verifying that the + * arguments provided were OK and the state transition was + * initiated. Begins the actual recovery logic. + * + * Returns an #ANASTASIS_ReduxAction. + * + * @param state state to operate on + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_recovery_challenge_begin_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * DispatchHandler/Callback function which is called for a + * "enter_user_attributes" action after verifying that the + * arguments provided were OK and the state transition was + * initiated. Begins the actual backup logic. + * + * Returns an #ANASTASIS_ReduxAction. + * + * @param state state to operate on + * @param cb callback to call during/after operation + * @param cb_cls callback closure + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_backup_begin_ (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 ctx the CURL context used to connect to the backend + * @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); + + +/** + * Generic container for an action with asynchronous activities. + */ +struct ANASTASIS_ReduxAction +{ + /** + * Function to call to clean up. + */ + void (*cleanup)(void *cls); + + /** + * Action-specific state, closure for @e cleanup. + */ + void *cleanup_cls; +}; + + +#endif diff --git a/src/reducer/validation_CH_AHV.c b/src/reducer/validation_CH_AHV.c new file mode 100644 index 0000000..e6a2f25 --- /dev/null +++ b/src/reducer/validation_CH_AHV.c @@ -0,0 +1,57 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file redux/validation_CH_AHV.c + * @brief anastasis reducer api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include <string.h> +#include <stdbool.h> + +/** + * Function to validate a Swiss AHV number. + * + * @param avh_number ahv number to validate (input) + * @return true if validation passed, else false + */ +bool +CH_AHV_check (const char *ahv_number) +{ + unsigned int checknum; + unsigned int next_ten; + const char *pos = &ahv_number[strlen (ahv_number) - 1]; + bool phase = true; + unsigned int calculation = 0; + + checknum = *pos - 48; + while (pos > ahv_number) + { + pos--; + if ('.' == *pos) + continue; + if (phase) + calculation += ((*pos - 48) * 3); + else + calculation += *pos - 48; + phase = ! phase; + } + /* round up to the next ten */ + next_ten = ((calculation + 9) / 10) * 10; + calculation = next_ten - calculation; + return (checknum == calculation); +} diff --git a/src/reducer/validation_CZ_BN.c b/src/reducer/validation_CZ_BN.c new file mode 100644 index 0000000..4598c42 --- /dev/null +++ b/src/reducer/validation_CZ_BN.c @@ -0,0 +1,59 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file redux/validation_CZ_BN.c + * @brief validation of Czeck Birth Numbers + * @author Christian Grothoff + */ +#include <string.h> +#include <stdbool.h> +#include <stdio.h> +#include <gcrypt.h> + +/** + * Function to validate a Check birth number. Basically, + * if it has 10 digits, it must be divisible by 11. + * + * @param b_number birth number to validate (input) + * @return true if b_number is valid + */ +bool +CZ_BN_check (const char *b_number) +{ + unsigned long long n; + char dummy; + char in[11]; + + if (10 == strlen (b_number)) + return true; + if (11 != strlen (b_number)) + return false; + if (b_number[6] != '/') + return false; + memcpy (in, + b_number, + 6); + memcpy (&in[6], + &b_number[7], + 4); + in[10] = '\0'; + if (1 != sscanf (in, + "%llu%c", + &n, + &dummy)) + return false; + return 0 == (n % 11); +} diff --git a/src/reducer/validation_DE_SVN.c b/src/reducer/validation_DE_SVN.c new file mode 100644 index 0000000..bfc406d --- /dev/null +++ b/src/reducer/validation_DE_SVN.c @@ -0,0 +1,98 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file redux/validation_DE_SVN.c + * @brief + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include <string.h> +#include <stdbool.h> + +/** + * Sum up all digits in @a v and return the result. + */ +static unsigned int +q (unsigned int v) +{ + unsigned int r = 0; + + while (0 != v) + { + r += v % 10; + v = v / 10; + } + return r; +} + + +/** + * Function to validate a German Social Security number. + * + * See https://www.financescout24.de/wissen/ratgeber/sozialversicherungsnummer + * and https://de.wikipedia.org/wiki/Versicherungsnummer + * for the structure! + * + * @param avh_number ahv number to validate (input) + * @return true if validation passed, else false + */ +bool +DE_SVN_check (const char *ssn_number) +{ + static const unsigned int factors[] = { + 2, 1, 2, 5, 7, 1, 2, 1, 2, 1, 2, 1 + }; + unsigned int sum = 0; + + if (strlen (ssn_number) != 12) + return false; + for (unsigned int i = 0; i<8; i++) + { + unsigned char c = (unsigned char) ssn_number[i]; + + if ( ('0' > c) || ('9' < c) ) + return false; + sum += q ((c - '0') * factors[i]); + } + { + unsigned char c = (unsigned char) ssn_number[8]; + unsigned int v = (c - 'A' + 1); + + if ( ('A' > c) || ('Z' < c) ) + return false; + sum += q ((v / 10) * factors[8]); + sum += q ((v % 10) * factors[9]); + } + for (unsigned int i = 9; i<11; i++) + { + unsigned char c = ssn_number[i]; + + if ( ('0' > c) || ('9' < c) ) + return false; + sum += q ((c - '0') * factors[i + 1]); + } + if (ssn_number[11] != '0' + (sum % 10)) + return false; + { + unsigned int month = (ssn_number[4] - '0') * 10 + (ssn_number[5] - '0'); + + if ( (0 == month) || + (12 < month) ) + return false; + } + return true; +} diff --git a/src/reducer/validation_DE_TIN.c b/src/reducer/validation_DE_TIN.c new file mode 100644 index 0000000..9c97365 --- /dev/null +++ b/src/reducer/validation_DE_TIN.c @@ -0,0 +1,57 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file redux/validation_DE_TIN.c + * @brief validation logic for German taxpayer identification numbers + * @author Christian Grothoff + */ +#include <string.h> +#include <stdbool.h> + + +/** + * Function to validate a German Taxpayer identification number. + * + * See https://de.wikipedia.org/wiki/Steuerliche_Identifikationsnummer + * for the structure! + * + * @param tin_number tax number to validate (input) + * @return true if validation passed, else false + */ +bool +DE_TIN_check (const char *tin_number) +{ + unsigned int csum; + unsigned int product = 10; + + if (strlen (tin_number) != 11) + return false; + for (unsigned int i = 0; i<10; i++) + { + unsigned int sum = ((tin_number[i] - '0') + product) % 10; + if (0 == sum) + sum = 10; + product = sum * 2 % 11; + } + csum = 11 - product; + if (10 == csum) + csum = 0; + if (tin_number[10] != '0' + csum) + return false; + if (tin_number[0] == '0') + return false; + return true; +} diff --git a/src/reducer/validation_IN_AADHAR.c b/src/reducer/validation_IN_AADHAR.c new file mode 100644 index 0000000..939ee72 --- /dev/null +++ b/src/reducer/validation_IN_AADHAR.c @@ -0,0 +1,113 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file redux/validation_IN_AADHAR.c + * @brief validation logic for Indian Aadhar numbers + * @author Christian Grothoff + */ +#include <string.h> +#include <stdbool.h> +#include <ctype.h> + +/** + * The multiplication table. + */ +static int m[10][10] = { + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 0, 6, 7, 8, 9, 5}, + {2, 3, 4, 0, 1, 7, 8, 9, 5, 6}, + {3, 4, 0, 1, 2, 8, 9, 5, 6, 7}, + {4, 0, 1, 2, 3, 9, 5, 6, 7, 8}, + {5, 9, 8, 7, 6, 0, 4, 3, 2, 1}, + {6, 5, 9, 8, 7, 1, 0, 4, 3, 2}, + {7, 6, 5, 9, 8, 2, 1, 0, 4, 3}, + {8, 7, 6, 5, 9, 3, 2, 1, 0, 4}, + {9, 8, 7, 6, 5, 4, 3, 2, 1, 0} +}; + + +/** + * The permutation table. + */ +static int p[10][10] = { + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 5, 7, 6, 2, 8, 3, 0, 9, 4}, + {5, 8, 0, 3, 7, 9, 6, 1, 4, 2}, + {8, 9, 1, 6, 0, 4, 3, 5, 2, 7}, + {9, 4, 5, 3, 1, 2, 6, 8, 7, 0}, + {4, 2, 8, 6, 5, 7, 3, 9, 0, 1}, + {2, 7, 9, 3, 8, 0, 6, 4, 1, 5}, + {7, 0, 4, 6, 9, 1, 3, 2, 5, 8} +}; + + +/** + * Converts a string to a reversed integer array. + * + * @param input The numeric string data converted to reversed int array. + * @param[out] Integer array containing the digits in the numeric string + * in reverse order + */ +static bool +string_to_vals (const char *input, + int output[12]) +{ + unsigned int off = 0; + + for (unsigned int i = 0; i < 12;) + { + int c = input[i + off]; + + if (0 == c) + return false; + if (isspace (c)) + { + off++; + continue; + } + if (! isdigit (c)) + return false; + output[11 - i++] = c - '0'; + } + if ('\0' != input[12 + off]) + return false; + return true; +} + + +/** + * Function to validate an Indian Aadhar number. + * + * See https://www.geeksforgeeks.org/how-to-check-aadhar-number-is-valid-or-not-using-regular-expression/ + * and http://en.wikipedia.org/wiki/Verhoeff_algorithm/. + * + * @param aadhar_number aadhar number to validate (input) + * @return true if validation passed, else false + */ +bool +IN_AADHAR_check (const char *aadhar_number) +{ + int c = 0; + int vals[12]; + + if (! string_to_vals (aadhar_number, + vals)) + return false; + for (unsigned int i = 0; i < 12; i++) + c = m[c][p[(i % 8)][vals[i]]]; + + return (0 == c); +} diff --git a/src/reducer/validation_IT_CF.c b/src/reducer/validation_IT_CF.c new file mode 100644 index 0000000..ecdad90 --- /dev/null +++ b/src/reducer/validation_IT_CF.c @@ -0,0 +1,198 @@ +/* + This file is part of Anastasis + Copyright (C) 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file redux/validation_IT_CF.c + * @brief validation of Italian Code Fiscales + * @author Christian Grothoff + */ +#include <string.h> +#include <stdbool.h> +#include <stdio.h> +#include <math.h> + +struct MapEntry +{ + char in; + unsigned int out; +}; + + +static const struct MapEntry odd[] = { + { '0', 1}, + { '9', 21}, + { 'I', 19 }, + { 'R', 8}, + { '1', 0}, + { 'A', 1}, + { 'J', 21}, + { 'S', 12}, + {'2', 5}, + {'B', 0}, + {'K', 2}, + {'T', 14}, + {'3', 7}, + {'C', 5}, + {'L', 4}, + {'U', 16}, + {'4', 9}, + {'D', 7}, + {'M', 18}, + {'V', 10}, + {'5', 13}, + {'E', 9}, + {'N', 20}, + {'W', 22}, + {'6', 15}, + {'F', 13}, + {'O', 11}, + {'X', 25}, + {'7', 17}, + {'G', 15}, + {'P', 3}, + {'Y', 24}, + {'8', 19}, + {'H', 17}, + {'Q', 6}, + {'Z', 23}, + {'\0', 0} +}; + + +static const struct MapEntry even[] = { + { '0', 0}, + { '1', 1}, + { '2', 2 }, + { '3', 3}, + { '4', 4}, + { '5', 5}, + { '6', 6 }, + { '7', 7 }, + {'8', 8}, + {'9', 9}, + {'A', 0}, + {'B', 1}, + {'C', 2}, + {'D', 3}, + {'E', 4}, + {'F', 5}, + {'G', 6}, + {'H', 7}, + {'I', 8}, + {'J', 9}, + {'K', 10}, + {'L', 11}, + {'M', 12}, + {'N', 13}, + {'O', 14}, + {'P', 15}, + {'Q', 16}, + {'R', 17}, + {'S', 18}, + {'T', 19}, + {'U', 20}, + {'V', 21}, + {'W', 22}, + {'X', 23}, + {'Y', 24}, + {'Z', 25}, + {'\0', 0} +}; + + +static const struct MapEntry rem[] = { + {'A', 0}, + {'B', 1}, + {'C', 2}, + {'D', 3}, + {'E', 4}, + {'F', 5}, + {'G', 6}, + {'H', 7}, + {'I', 8}, + {'J', 9}, + {'K', 10}, + {'L', 11}, + {'M', 12}, + {'N', 13}, + {'O', 14}, + {'P', 15}, + {'Q', 16}, + {'R', 17}, + {'S', 18}, + {'T', 19}, + {'U', 20}, + {'V', 21}, + {'W', 22}, + {'X', 23}, + {'Y', 24}, + {'Z', 25}, + {'\0', 0} +}; + + +/** + * Lookup @a in in @a map. Set @a fail to true if @a in is not found. + * + * @param map character map to search + * @param in character to search for + * @param[out] fail set to true on error + * @return map value, 0 on error + */ +static unsigned int +lookup (const struct MapEntry *map, + char in, + bool *fail) +{ + for (unsigned int i = 0; '\0' != map[i].in; i++) + if (in == map[i].in) + return map[i].out; + *fail = true; + return 0; +} + + +/** + * Function to validate an italian code fiscale number. + * See https://en.wikipedia.org/wiki/Italian_fiscal_code + * + * @param cf_number square number to validate (input) + * @return true if @a cf_number is a valid, else false + */ +bool +IT_CF_check (const char *cf_number) +{ + unsigned int sum = 0; + bool fail = false; + + if (strlen (cf_number) != 16) + return false; + for (unsigned int i = 0; i<15; i += 2) + sum += lookup (odd, + cf_number[i], + &fail); + for (unsigned int i = 1; i<15; i += 2) + sum += lookup (even, + cf_number[i], + &fail); + sum %= 26; + if (sum != lookup (rem, + cf_number[15], + &fail)) + return false; + if (fail) + return false; + return true; +} diff --git a/src/reducer/validation_XX_SQUARE.c b/src/reducer/validation_XX_SQUARE.c new file mode 100644 index 0000000..fa3ebfb --- /dev/null +++ b/src/reducer/validation_XX_SQUARE.c @@ -0,0 +1,48 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file redux/validation_XX_PRIME.c + * @brief anastasis reducer api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include <string.h> +#include <stdbool.h> +#include <stdio.h> +#include <math.h> + +/** + * Function to validate a square number. + * + * @param sq_number square number to validate (input) + * @return true if sq_number is a square, else false + */ +bool +XX_SQUARE_check (const char *sq_number) +{ + unsigned long long n; + unsigned long long r; + char dummy; + + if (1 != sscanf (sq_number, + "%llu%c", + &n, + &dummy)) + return false; + r = sqrt (n); + return (n == r * r); +} diff --git a/src/reducer/validation_XY_PRIME.c b/src/reducer/validation_XY_PRIME.c new file mode 100644 index 0000000..ab24014 --- /dev/null +++ b/src/reducer/validation_XY_PRIME.c @@ -0,0 +1,53 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file redux/validation_XY_PRIME.c + * @brief anastasis reducer api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include <string.h> +#include <stdbool.h> +#include <stdio.h> +#include <gcrypt.h> + +/** + * Function to validate a prime number. + * + * @param pr_number prime number to validate (input) + * @return true if pr_number is a prime, else false + */ +bool +XY_PRIME_check (const char *pr_number) +{ + unsigned long long n; + char dummy; + gcry_mpi_t p; + bool is_prime; + + if (1 != sscanf (pr_number, + "%llu%c", + &n, + &dummy)) + return false; + p = gcry_mpi_set_ui (NULL, + (unsigned long) n); + is_prime = (0 == gcry_prime_check (p, + 0)); + gcry_mpi_release (p); + return is_prime; +} |