anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

commit 7615eedae52510f3d56058350b62934f7e720a31
parent aa9e1d0e40684eda203846981f2b24e17043f003
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed, 29 Jul 2026 11:01:11 +0200

add parser/serializer for redux state, remove dead truth_lifetime

Diffstat:
Mdoc/anastasis.texi | 4----
Mdoc/sphinx/reducer.rst | 4----
Msrc/cli/resources/03-backup.json | 12------------
Msrc/cli/resources/04-backup.json | 12------------
Msrc/cli/resources/05-backup.json | 12------------
Msrc/cli/resources/06-backup.json | 12------------
Asrc/reducer/anastasis_api_redux_free.c | 284+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/reducer/anastasis_api_redux_parse.c | 2193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/reducer/anastasis_api_redux_serialize.c | 881+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/reducer/anastasis_api_redux_state.c | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/reducer/anastasis_api_redux_state.h | 1446+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/reducer/test_redux_codec.c | 365+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 5316 insertions(+), 56 deletions(-)

diff --git a/doc/anastasis.texi b/doc/anastasis.texi @@ -3411,7 +3411,6 @@ providers that accept payments in the selected currency: "truth_upload_fee": "EUR:4.99", "liability_limit": "EUR:1", "currency": "EUR", - "truth_lifetime": @{ "d_ms" : 50000000 @}, "storage_limit_in_megabytes": 1, "provider_name": "Anastasis 4", "salt": "CXAPCKSH9D3MYJTS9536RHJHCW" @@ -3428,7 +3427,6 @@ providers that accept payments in the selected currency: "truth_upload_fee": "EUR:3.99", "liability_limit": "EUR:1", "currency": "EUR", - "truth_lifetime": @{ "d_ms" : 50000000 @}, "storage_limit_in_megabytes": 1, "provider_name": "Anastasis 4", "salt": "CXAPCKSH9D3MYJTS9536RHJHCW" @@ -3606,7 +3604,6 @@ port 8088 was now added, and on 8090 is disabled: "truth_upload_fee": "EUR:3.99", "liability_limit": "EUR:1", "currency": "EUR", - "truth_lifetime": @{ "d_ms" : 50000000 @}, "storage_limit_in_megabytes": 1, "provider_name": "Anastasis 4", "salt": "CXAPCKSH9D3MYJTS9536RHJHCW" @@ -3622,7 +3619,6 @@ port 8088 was now added, and on 8090 is disabled: "truth_upload_fee": "EUR:3.99", "liability_limit": "EUR:1", "currency": "EUR", - "truth_lifetime": @{ "d_ms" : 50000000 @}, "storage_limit_in_megabytes": 1, "provider_name": "Anastasis 42", "salt": "BXAPCKSH9D3MYJTS9536RHJHCX" diff --git a/doc/sphinx/reducer.rst b/doc/sphinx/reducer.rst @@ -409,7 +409,6 @@ providers that accept payments in the selected currency: "truth_upload_fee": "EUR:4.99", "liability_limit": "EUR:1", "currency": "EUR", - "truth_lifetime": { "d_ms" : 50000000 }, "storage_limit_in_megabytes": 1, "provider_name": "Anastasis 4", "salt": "CXAPCKSH9D3MYJTS9536RHJHCW" @@ -426,7 +425,6 @@ providers that accept payments in the selected currency: "truth_upload_fee": "EUR:3.99", "liability_limit": "EUR:1", "currency": "EUR", - "truth_lifetime": { "d_ms" : 50000000 }, "storage_limit_in_megabytes": 1, "provider_name": "Anastasis 4", "salt": "CXAPCKSH9D3MYJTS9536RHJHCW" @@ -549,7 +547,6 @@ port 8088 was now added, and on 8090 is disabled: "truth_upload_fee": "EUR:3.99", "liability_limit": "EUR:1", "currency": "EUR", - "truth_lifetime": { "d_ms" : 50000000 }, "storage_limit_in_megabytes": 1, "provider_name": "Anastasis 4", "salt": "CXAPCKSH9D3MYJTS9536RHJHCW" @@ -565,7 +562,6 @@ port 8088 was now added, and on 8090 is disabled: "truth_upload_fee": "EUR:3.99", "liability_limit": "EUR:1", "currency": "EUR", - "truth_lifetime": { "d_ms" : 50000000 }, "storage_limit_in_megabytes": 1, "provider_name": "Anastasis 42", "salt": "BXAPCKSH9D3MYJTS9536RHJHCX" diff --git a/src/cli/resources/03-backup.json b/src/cli/resources/03-backup.json @@ -32,9 +32,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "business_name": "Data loss #1 Inc.", "storage_limit_in_megabytes": 1, "provider_salt": "F0HEYJQW81ZAZ3VYMZHFG8T1Z0", @@ -51,9 +48,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "business_name": "Data loss #2 Inc.", "storage_limit_in_megabytes": 1, "provider_salt": "D378FWXHJB8JHPQFQRZGGV9PWG", @@ -70,9 +64,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "business_name": "Data loss #3 Inc.", "storage_limit_in_megabytes": 1, "provider_salt": "7W9W4A4TTWSWRPJ76RNDPJHSPR", @@ -89,9 +80,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "business_name": "Data loss #4 Inc.", "storage_limit_in_megabytes": 1, "provider_salt": "PN0VJF6KDSBYN40SGRCEXPB07M", diff --git a/src/cli/resources/04-backup.json b/src/cli/resources/04-backup.json @@ -32,9 +32,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "business_name": "Data loss #1 Inc.", "storage_limit_in_megabytes": 1, "provider_salt": "F0HEYJQW81ZAZ3VYMZHFG8T1Z0", @@ -51,9 +48,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "business_name": "Data loss #2 Inc.", "storage_limit_in_megabytes": 1, "provider_salt": "D378FWXHJB8JHPQFQRZGGV9PWG", @@ -70,9 +64,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "business_name": "Data loss #3 Inc.", "storage_limit_in_megabytes": 1, "provider_salt": "7W9W4A4TTWSWRPJ76RNDPJHSPR", @@ -89,9 +80,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "business_name": "Data loss #4 Inc.", "storage_limit_in_megabytes": 1, "provider_salt": "PN0VJF6KDSBYN40SGRCEXPB07M", diff --git a/src/cli/resources/05-backup.json b/src/cli/resources/05-backup.json @@ -51,9 +51,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "currency": "TESTKUDOS", "business_name": "Data loss #1 Inc.", "storage_limit_in_megabytes": 1, @@ -71,9 +68,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "currency": "TESTKUDOS", "business_name": "Data loss #2 Inc.", "storage_limit_in_megabytes": 1, @@ -91,9 +85,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "currency": "TESTKUDOS", "business_name": "Data loss #3 Inc.", "storage_limit_in_megabytes": 1, @@ -111,9 +102,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "currency": "TESTKUDOS", "business_name": "Data loss #4 Inc.", "storage_limit_in_megabytes": 1, diff --git a/src/cli/resources/06-backup.json b/src/cli/resources/06-backup.json @@ -51,9 +51,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "currency": "TESTKUDOS", "business_name": "Data loss #1 Inc.", "storage_limit_in_megabytes": 1, @@ -71,9 +68,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "currency": "TESTKUDOS", "business_name": "Data loss #2 Inc.", "storage_limit_in_megabytes": 1, @@ -91,9 +85,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "currency": "TESTKUDOS", "business_name": "Data loss #3 Inc.", "storage_limit_in_megabytes": 1, @@ -111,9 +102,6 @@ "annual_fee": "TESTKUDOS:4.99", "truth_upload_fee": "TESTKUDOS:0.01", "liability_limit": "TESTKUDOS:1", - "truth_lifetime": { - "d_ms": 63115200000 - }, "currency": "TESTKUDOS", "business_name": "Data loss #4 Inc.", "storage_limit_in_megabytes": 1, diff --git a/src/reducer/anastasis_api_redux_free.c b/src/reducer/anastasis_api_redux_free.c @@ -0,0 +1,284 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 2021, 2022 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file reducer/anastasis_api_redux_free.c + * @brief release a parsed reducer state + * @author Christian Grothoff + * + * Every structure here is allocated zeroed (GNUNET_new(_array)) and every + * array's length field is set before the array is filled, so this code also + * has to cope with partially initialized states: the parser simply hands a + * half-built state to ANASTASIS_REDUX_state_free_() when it gives up. + */ +#include "platform.h" +#include "anastasis_redux.h" +#include "anastasis_api_redux.h" +#include "anastasis_api_redux_state.h" + + +void +ANASTASIS_REDUX_provider_url_clear_ (struct ANASTASIS_ProviderUrl *pu) +{ + GNUNET_free (pu->url); +} + + +void +ANASTASIS_REDUX_providers_clear_ (struct ANASTASIS_ReduxCommon *common) +{ + for (unsigned int i = 0; i < common->providers_len; i++) + { + struct ANASTASIS_ReduxProvider *p = &common->providers[i]; + + ANASTASIS_REDUX_provider_url_clear_ (&p->url); + if (p->have_config) + { + for (unsigned int j = 0; j < p->config.methods_len; j++) + GNUNET_free (p->config.methods[j].type); + GNUNET_free (p->config.methods); + GNUNET_free (p->config.business_name); + GNUNET_free (p->config.currency); + } + } + GNUNET_free (common->providers); + common->providers = NULL; + common->providers_len = 0; +} + + +/** + * Free the contents of @a common. + * + * @param[in,out] common state to free + */ +static void +free_common (struct ANASTASIS_ReduxCommon *common) +{ + for (unsigned int i = 0; i < common->continents_len; i++) + { + struct ANASTASIS_ReduxContinent *c = &common->continents[i]; + + GNUNET_free (c->name); + json_decref (c->name_i18n); + } + GNUNET_free (common->continents); + for (unsigned int i = 0; i < common->countries_len; i++) + { + struct ANASTASIS_ReduxCountry *c = &common->countries[i]; + + GNUNET_free (c->code); + GNUNET_free (c->name); + GNUNET_free (c->continent); + GNUNET_free (c->call_code); + GNUNET_free (c->currency); + json_decref (c->name_i18n); + json_decref (c->continent_i18n); + } + GNUNET_free (common->countries); + for (unsigned int i = 0; i < common->required_attributes_len; i++) + { + struct ANASTASIS_ReduxAttributeSpec *a = &common->required_attributes[i]; + + GNUNET_free (a->type); + GNUNET_free (a->name); + GNUNET_free (a->label); + GNUNET_free (a->widget); + GNUNET_free (a->uuid); + GNUNET_free (a->tooltip); + GNUNET_free (a->validation_regex); + GNUNET_free (a->validation_logic); + GNUNET_free (a->autocomplete); + json_decref (a->label_i18n); + } + GNUNET_free (common->required_attributes); + ANASTASIS_REDUX_providers_clear_ (common); + for (unsigned int i = 0; i < common->currencies_len; i++) + GNUNET_free (common->currencies[i]); + GNUNET_free (common->currencies); + json_decref (common->identity_attributes); + GNUNET_free (common->selected_continent); + GNUNET_free (common->selected_country); +} + + +/** + * Free the contents of @a backup. + * + * @param[in,out] backup state to free + */ +static void +free_backup (struct ANASTASIS_ReduxBackup *backup) +{ + for (unsigned int i = 0; i < backup->authentication_methods_len; i++) + { + struct ANASTASIS_ReduxAuthMethod *am = &backup->authentication_methods[i]; + + GNUNET_free (am->type); + GNUNET_free (am->instructions); + GNUNET_free (am->challenge); + GNUNET_free (am->mime_type); + } + GNUNET_free (backup->authentication_methods); + for (unsigned int i = 0; i < backup->policies_len; i++) + { + struct ANASTASIS_ReduxPolicy *p = &backup->policies[i]; + + for (unsigned int j = 0; j < p->methods_len; j++) + { + struct ANASTASIS_ReduxPolicyMethod *pm = &p->methods[j]; + + ANASTASIS_REDUX_provider_url_clear_ (&pm->provider); + if (NULL != pm->truth) + ANASTASIS_truth_free (pm->truth); + } + GNUNET_free (p->methods); + } + GNUNET_free (backup->policies); + for (unsigned int i = 0; i < backup->policy_providers_len; i++) + ANASTASIS_REDUX_provider_url_clear_ ( + &backup->policy_providers[i].provider_url); + GNUNET_free (backup->policy_providers); + GNUNET_free (backup->upload_fees); + for (unsigned int i = 0; i < backup->payments_len; i++) + GNUNET_free (backup->payments[i]); + GNUNET_free (backup->payments); + for (unsigned int i = 0; i < backup->policy_payment_requests_len; i++) + { + struct ANASTASIS_ReduxPolicyPaymentRequest *ppr + = &backup->policy_payment_requests[i]; + + GNUNET_free (ppr->payto); + ANASTASIS_REDUX_provider_url_clear_ (&ppr->provider); + } + GNUNET_free (backup->policy_payment_requests); + for (unsigned int i = 0; i < backup->success_details_len; i++) + ANASTASIS_REDUX_provider_url_clear_ ( + &backup->success_details[i].provider_url); + GNUNET_free (backup->success_details); + json_decref (backup->core_secret); + GNUNET_free (backup->secret_name); +} + + +/** + * Free the variant-specific contents of @a fb, but not @a fb itself. + * + * @param[in,out] fb feedback to reset + */ +void +ANASTASIS_REDUX_feedback_clear_ (struct ANASTASIS_ReduxFeedback *fb) +{ + GNUNET_free (fb->display_hint); + switch (fb->status) + { + case ANASTASIS_RFS_CODE_IN_FILE: + GNUNET_free (fb->details.code_in_file.filename); + break; + case ANASTASIS_RFS_SEND_TO_ADDRESS: + GNUNET_free (fb->details.send_to_address.address_hint); + break; + case ANASTASIS_RFS_TALER_PAYMENT: + GNUNET_free (fb->details.taler_payment.taler_pay_uri); + ANASTASIS_REDUX_provider_url_clear_ (&fb->details.taler_payment.provider); + break; + case ANASTASIS_RFS_IBAN_INSTRUCTIONS: + GNUNET_free (fb->details.iban_instructions.target_iban); + GNUNET_free (fb->details.iban_instructions.target_business_name); + GNUNET_free (fb->details.iban_instructions.wire_transfer_subject); + break; + case ANASTASIS_RFS_SERVER_FAILURE: + case ANASTASIS_RFS_TRUTH_UNKNOWN: + case ANASTASIS_RFS_SOLVED: + case ANASTASIS_RFS_INCORRECT_ANSWER: + case ANASTASIS_RFS_RATE_LIMIT_EXCEEDED: + /* nothing heap-allocated beyond the display hint */ + break; + } + memset (&fb->details, + 0, + sizeof (fb->details)); + fb->display_hint = NULL; +} + + +/** + * Free the recovery information @a ri. + * + * @param[in] ri recovery information to free, may be NULL + */ +static void +free_recovery_info (struct ANASTASIS_ReduxRecoveryInfo *ri) +{ + if (NULL == ri) + return; + for (unsigned int i = 0; i < ri->challenges_len; i++) + { + GNUNET_free (ri->challenges[i].type); + GNUNET_free (ri->challenges[i].instructions); + } + GNUNET_free (ri->challenges); + for (unsigned int i = 0; i < ri->policies_len; i++) + GNUNET_free (ri->policies[i].uuids); + GNUNET_free (ri->policies); + GNUNET_free (ri->secret_name); + ANASTASIS_REDUX_provider_url_clear_ (&ri->provider_url); + GNUNET_free (ri); +} + + +/** + * Free the contents of @a recovery. + * + * @param[in,out] recovery state to free + */ +static void +free_recovery (struct ANASTASIS_ReduxRecovery *recovery) +{ + if (NULL != recovery->r) + ANASTASIS_recovery_abort (recovery->r); + free_recovery_info (recovery->ri); + for (unsigned int i = 0; i < recovery->challenge_feedback_len; i++) + ANASTASIS_REDUX_feedback_clear_ (&recovery->challenge_feedback[i]); + GNUNET_free (recovery->challenge_feedback); + json_decref (recovery->core_secret); +} + + +void +ANASTASIS_REDUX_state_free_ (struct ANASTASIS_ReduxState *rs) +{ + if (NULL == rs) + return; + switch (rs->type) + { + case ANASTASIS_RT_BACKUP: + free_backup (&rs->details.backup); + free_common (&rs->common); + break; + case ANASTASIS_RT_RECOVERY: + free_recovery (&rs->details.recovery); + free_common (&rs->common); + break; + case ANASTASIS_RT_ERROR: + GNUNET_free (rs->details.error.hint); + GNUNET_free (rs->details.error.detail); + break; + } + GNUNET_free (rs); +} + + +/* end of anastasis_api_redux_free.c */ diff --git a/src/reducer/anastasis_api_redux_parse.c b/src/reducer/anastasis_api_redux_parse.c @@ -0,0 +1,2193 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 2021, 2022 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file reducer/anastasis_api_redux_parse.c + * @brief parse a reducer state from its JSON encoding + * @author Christian Grothoff + * + * Parsing is strict: every object is checked against the closed set of + * fields declared for it, so a typo or a field from a foreign reducer + * implementation is an error rather than something silently carried along. + * + * On any failure the partially built state is handed to + * ANASTASIS_REDUX_state_free_(), which is safe because every array length is + * assigned before the array is populated. + */ +#include "platform.h" +#include "anastasis_redux.h" +#include "anastasis_api_redux.h" +#include "anastasis_api_redux_state.h" +#include <taler/taler_json_lib.h> + + +/** + * Buffer for the error detail returned by + * ANASTASIS_REDUX_state_parse_(). The reducer is single-threaded (it + * runs off the GNUnet scheduler), and the caller copies the detail into + * its error state immediately, so one static buffer is enough. + */ +static char parse_detail[256]; + + +/** + * Record @a field as the location of a parse error. + * + * @param where name of the enclosing object + * @param field offending field, may be NULL + * @return pointer to the formatted detail + */ +static const char * +set_detail (const char *where, + const char *field) +{ + if (NULL == field) + GNUNET_snprintf (parse_detail, + sizeof (parse_detail), + "%s", + where); + else + GNUNET_snprintf (parse_detail, + sizeof (parse_detail), + "%s.%s", + where, + field); + return parse_detail; +} + + +/** + * Check that @a obj is an object and has no members beyond those listed + * in @a allowed. + * + * @param obj object to check + * @param where name of @a obj, for error reporting + * @param allowed NULL-terminated array of permitted field names + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK if @a obj is acceptable + */ +static enum GNUNET_GenericReturnValue +check_fields (const json_t *obj, + const char *where, + const char *const*allowed, + const char **detail) +{ + const char *key; + json_t *val; + + if (! json_is_object (obj)) + { + *detail = set_detail (where, + NULL); + return GNUNET_SYSERR; + } + json_object_foreach ((json_t *) obj, key, val) + { + bool found = false; + + for (unsigned int i = 0; NULL != allowed[i]; i++) + if (0 == strcmp (key, + allowed[i])) + { + found = true; + break; + } + if (! found) + { + *detail = set_detail (where, + key); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * Duplicate @a s, or return NULL if @a s is NULL. + * + * @param s string to duplicate + * @return copy of @a s + */ +static char * +dup_or_null (const char *s) +{ + if (NULL == s) + return NULL; + return GNUNET_strdup (s); +} + + +/** + * Take a reference to the i18n map @a obj, or return NULL. + * + * The reducer never looks inside a translation map; it only ever carries + * it from the resource files to the user interface. So there is nothing + * to type here, and the object is kept verbatim. + * + * @param obj object mapping language tags to strings, may be NULL + * @return new reference to @a obj, or NULL + */ +static json_t * +incref_or_null (const json_t *obj) +{ + if (NULL == obj) + return NULL; + return json_incref ((json_t *) obj); +} + + +/** + * Parse the `continents` array. + * + * @param arr the array, may be NULL + * @param[out] common state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_continents (const json_t *arr, + struct ANASTASIS_ReduxCommon *common, + const char **detail) +{ + static const char *const allowed[] = { + "name", + "name_i18n", + NULL + }; + size_t index; + json_t *val; + + if (NULL == arr) + return GNUNET_OK; + if (! json_is_array (arr)) + { + *detail = set_detail ("continents", + NULL); + return GNUNET_SYSERR; + } + common->have_continents = true; + common->continents_len = (unsigned int) json_array_size (arr); + common->continents = GNUNET_new_array (common->continents_len, + struct ANASTASIS_ReduxContinent); + json_array_foreach (arr, index, val) + { + struct ANASTASIS_ReduxContinent *c = &common->continents[index]; + const char *name; + const json_t *i18n = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &name), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("name_i18n", + &i18n), + NULL), + GNUNET_JSON_spec_end () + }; + + if (json_is_string (val)) + { + /* hand-written states name continents directly */ + c->name = GNUNET_strdup (json_string_value (val)); + c->bare = true; + continue; + } + if (GNUNET_OK != + check_fields (val, + "continents", + allowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + spec, + NULL, NULL)) + { + *detail = set_detail ("continents", + "name"); + return GNUNET_SYSERR; + } + c->name = GNUNET_strdup (name); + c->name_i18n = incref_or_null (i18n); + } + return GNUNET_OK; +} + + +/** + * Parse the `countries` array. + * + * @param arr the array, may be NULL + * @param[out] common state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_countries (const json_t *arr, + struct ANASTASIS_ReduxCommon *common, + const char **detail) +{ + static const char *const allowed[] = { + "code", + "name", + "continent", + "call_code", + "name_i18n", + "continent_i18n", + "currency", + NULL + }; + size_t index; + json_t *val; + + if (NULL == arr) + return GNUNET_OK; + if (! json_is_array (arr)) + { + *detail = set_detail ("countries", + NULL); + return GNUNET_SYSERR; + } + common->have_countries = true; + common->countries_len = (unsigned int) json_array_size (arr); + common->countries = GNUNET_new_array (common->countries_len, + struct ANASTASIS_ReduxCountry); + json_array_foreach (arr, index, val) + { + struct ANASTASIS_ReduxCountry *c = &common->countries[index]; + const char *code; + const char *name; + const char *continent; + const char *call_code = NULL; + const char *currency = NULL; + const json_t *name_i18n = NULL; + const json_t *continent_i18n = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("code", + &code), + GNUNET_JSON_spec_string ("name", + &name), + GNUNET_JSON_spec_string ("continent", + &continent), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("call_code", + &call_code), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("name_i18n", + &name_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("continent_i18n", + &continent_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("currency", + &currency), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + check_fields (val, + "countries", + allowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + spec, + NULL, NULL)) + { + *detail = set_detail ("countries", + NULL); + return GNUNET_SYSERR; + } + c->code = GNUNET_strdup (code); + c->name = GNUNET_strdup (name); + c->continent = GNUNET_strdup (continent); + c->call_code = dup_or_null (call_code); + c->currency = dup_or_null (currency); + c->name_i18n = incref_or_null (name_i18n); + c->continent_i18n = incref_or_null (continent_i18n); + } + return GNUNET_OK; +} + + +/** + * Parse the `required_attributes` array. + * + * @param arr the array, may be NULL + * @param[out] common state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_required_attributes (const json_t *arr, + struct ANASTASIS_ReduxCommon *common, + const char **detail) +{ + static const char *const allowed[] = { + "type", + "name", + "label", + "widget", + "uuid", + "tooltip", + "label_i18n", + "validation-regex", + "validation-logic", + "autocomplete", + "optional", + NULL + }; + size_t index; + json_t *val; + + if (NULL == arr) + return GNUNET_OK; + if (! json_is_array (arr)) + { + *detail = set_detail ("required_attributes", + NULL); + return GNUNET_SYSERR; + } + common->have_required_attributes = true; + common->required_attributes_len = (unsigned int) json_array_size (arr); + common->required_attributes + = GNUNET_new_array (common->required_attributes_len, + struct ANASTASIS_ReduxAttributeSpec); + json_array_foreach (arr, index, val) + { + struct ANASTASIS_ReduxAttributeSpec *a + = &common->required_attributes[index]; + const char *type; + const char *name; + const char *label; + const char *widget; + const char *uuid = NULL; + const char *tooltip = NULL; + const char *regex = NULL; + const char *logic = NULL; + const char *autocomplete = NULL; + const json_t *label_i18n = NULL; + bool no_optional; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_string ("name", + &name), + GNUNET_JSON_spec_string ("label", + &label), + GNUNET_JSON_spec_string ("widget", + &widget), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("uuid", + &uuid), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("tooltip", + &tooltip), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("validation-regex", + &regex), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("validation-logic", + &logic), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("autocomplete", + &autocomplete), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("label_i18n", + &label_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("optional", + &a->optional), + &no_optional), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + check_fields (val, + "required_attributes", + allowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + spec, + NULL, NULL)) + { + *detail = set_detail ("required_attributes", + NULL); + return GNUNET_SYSERR; + } + a->type = GNUNET_strdup (type); + a->name = GNUNET_strdup (name); + a->label = GNUNET_strdup (label); + a->widget = GNUNET_strdup (widget); + a->uuid = dup_or_null (uuid); + a->tooltip = dup_or_null (tooltip); + a->validation_regex = dup_or_null (regex); + a->validation_logic = dup_or_null (logic); + a->autocomplete = dup_or_null (autocomplete); + a->have_optional = ! no_optional; + a->label_i18n = incref_or_null (label_i18n); + } + return GNUNET_OK; +} + + +/** + * Parse one entry of the `authentication_providers` object. + * + * @param url the provider URL (the key) + * @param val the provider details (the value) + * @param[out] p provider entry to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_provider (const char *url, + const json_t *val, + struct ANASTASIS_ReduxProvider *p, + const char **detail) +{ + static const char *const allowed[] = { + "status", + "methods", + "annual_fee", + "truth_upload_fee", + "liability_limit", + "business_name", + "storage_limit_in_megabytes", + "provider_salt", + "http_status", + "error_code", + "currency", + NULL + }; + const char *status; + struct GNUNET_JSON_Specification sspec[] = { + GNUNET_JSON_spec_string ("status", + &status), + GNUNET_JSON_spec_end () + }; + + ANASTASIS_REDUX_provider_url_set_ (&p->url, + url); + if (GNUNET_OK != + check_fields (val, + "authentication_providers", + allowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + sspec, + NULL, NULL)) + { + *detail = set_detail ("authentication_providers", + "status"); + return GNUNET_SYSERR; + } + if (0 == strcmp (status, + "not-contacted")) + { + p->status = ANASTASIS_RPS_NOT_CONTACTED; + return GNUNET_OK; + } + if (0 == strcmp (status, + "error")) + { + uint64_t ec = 0; + struct GNUNET_JSON_Specification espec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("error_code", + &ec), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("http_status", + &p->error.http_status), + NULL), + GNUNET_JSON_spec_end () + }; + + p->status = ANASTASIS_RPS_ERROR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + espec, + NULL, NULL)) + { + *detail = set_detail ("authentication_providers", + "error_code"); + return GNUNET_SYSERR; + } + p->error.ec = (enum TALER_ErrorCode) ec; + return GNUNET_OK; + } + if (0 == strcmp (status, + "ok")) + p->status = ANASTASIS_RPS_OK; + else if (0 == strcmp (status, + "disabled")) + p->status = ANASTASIS_RPS_DISABLED; + else + { + *detail = set_detail ("authentication_providers", + "status"); + return GNUNET_SYSERR; + } + + /* Both "ok" and (usually) "disabled" carry the /config we obtained + earlier; a provider that was disabled before we ever talked to it + does not. */ + { + struct ANASTASIS_ReduxProviderConfig *cfg = &p->config; + const char *business_name; + const char *currency = NULL; + const json_t *methods; + bool no_config = false; + struct GNUNET_JSON_Specification cspec[] = { + GNUNET_JSON_spec_array_const ("methods", + &methods), + TALER_JSON_spec_amount_any ("annual_fee", + &cfg->annual_fee), + TALER_JSON_spec_amount_any ("truth_upload_fee", + &cfg->truth_upload_fee), + TALER_JSON_spec_amount_any ("liability_limit", + &cfg->liability_limit), + GNUNET_JSON_spec_string ("business_name", + &business_name), + GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", + &cfg->storage_limit_in_megabytes), + GNUNET_JSON_spec_fixed_auto ("provider_salt", + &cfg->provider_salt), + GNUNET_JSON_spec_uint32 ("http_status", + &cfg->http_status), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("currency", + &currency), + NULL), + GNUNET_JSON_spec_end () + }; + size_t index; + json_t *m; + + if (NULL == json_object_get (val, + "methods")) + no_config = true; + if (no_config) + { + if (ANASTASIS_RPS_OK == p->status) + { + *detail = set_detail ("authentication_providers", + "methods"); + return GNUNET_SYSERR; + } + return GNUNET_OK; + } + if (GNUNET_OK != + GNUNET_JSON_parse (val, + cspec, + NULL, NULL)) + { + *detail = set_detail ("authentication_providers", + NULL); + return GNUNET_SYSERR; + } + p->have_config = true; + cfg->business_name = GNUNET_strdup (business_name); + cfg->currency = dup_or_null (currency); + cfg->methods_len = (unsigned int) json_array_size (methods); + cfg->methods = GNUNET_new_array (cfg->methods_len, + struct ANASTASIS_ReduxMethodSpec); + json_array_foreach (methods, index, m) + { + static const char *const mallowed[] = { + "type", + "usage_fee", + NULL + }; + struct ANASTASIS_ReduxMethodSpec *ms = &cfg->methods[index]; + const char *type; + struct GNUNET_JSON_Specification mspec[] = { + GNUNET_JSON_spec_string ("type", + &type), + TALER_JSON_spec_amount_any ("usage_fee", + &ms->usage_fee), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + check_fields (m, + "authentication_providers.methods", + mallowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (m, + mspec, + NULL, NULL)) + { + *detail = set_detail ("authentication_providers.methods", + NULL); + return GNUNET_SYSERR; + } + ms->type = GNUNET_strdup (type); + } + } + return GNUNET_OK; +} + + +/** + * Parse the `authentication_providers` object. + * + * @param obj the object, may be NULL + * @param[out] common state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_providers (const json_t *obj, + struct ANASTASIS_ReduxCommon *common, + const char **detail) +{ + const char *url; + json_t *val; + unsigned int off = 0; + + if (NULL == obj) + return GNUNET_OK; + if (! json_is_object (obj)) + { + *detail = set_detail ("authentication_providers", + NULL); + return GNUNET_SYSERR; + } + common->have_providers = true; + common->providers_len = (unsigned int) json_object_size (obj); + common->providers = GNUNET_new_array (common->providers_len, + struct ANASTASIS_ReduxProvider); + json_object_foreach ((json_t *) obj, url, val) + { + if (GNUNET_OK != + parse_provider (url, + val, + &common->providers[off], + detail)) + return GNUNET_SYSERR; + off++; + } + return GNUNET_OK; +} + + +/** + * Parse the fields shared by the backup and recovery variants. + * + * @param json the state + * @param[out] common state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_common (const json_t *json, + struct ANASTASIS_ReduxCommon *common, + const char **detail) +{ + const json_t *continents = NULL; + const json_t *countries = NULL; + const json_t *required_attributes = NULL; + const json_t *providers = NULL; + const json_t *identity_attributes = NULL; + const json_t *currencies = NULL; + const char *selected_continent = NULL; + const char *selected_country = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("continents", + &continents), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("countries", + &countries), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("required_attributes", + &required_attributes), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("authentication_providers", + &providers), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("identity_attributes", + &identity_attributes), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("currencies", + &currencies), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("selected_continent", + &selected_continent), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("selected_country", + &selected_country), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + *detail = set_detail ("state", + NULL); + return GNUNET_SYSERR; + } + common->selected_continent = dup_or_null (selected_continent); + common->selected_country = dup_or_null (selected_country); + common->identity_attributes = json_incref ((json_t *) identity_attributes); + if (NULL != currencies) + { + size_t index; + json_t *val; + + common->have_currencies = true; + common->currencies_len = (unsigned int) json_array_size (currencies); + common->currencies = GNUNET_new_array (common->currencies_len, + char *); + json_array_foreach (currencies, index, val) + { + if (! json_is_string (val)) + { + *detail = set_detail ("currencies", + NULL); + return GNUNET_SYSERR; + } + common->currencies[index] = GNUNET_strdup (json_string_value (val)); + } + } + if ( (GNUNET_OK != + parse_continents (continents, + common, + detail)) || + (GNUNET_OK != + parse_countries (countries, + common, + detail)) || + (GNUNET_OK != + parse_required_attributes (required_attributes, + common, + detail)) || + (GNUNET_OK != + parse_providers (providers, + common, + detail)) ) + return GNUNET_SYSERR; + return GNUNET_OK; +} + + +/** + * Parse the `authentication_methods` array. + * + * @param arr the array, may be NULL + * @param[out] b backup state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_auth_methods (const json_t *arr, + struct ANASTASIS_ReduxBackup *b, + const char **detail) +{ + static const char *const allowed[] = { + "type", + "instructions", + "challenge", + "mime_type", + NULL + }; + size_t index; + json_t *val; + + if (NULL == arr) + return GNUNET_OK; + b->have_authentication_methods = true; + b->authentication_methods_len = (unsigned int) json_array_size (arr); + b->authentication_methods + = GNUNET_new_array (b->authentication_methods_len, + struct ANASTASIS_ReduxAuthMethod); + json_array_foreach (arr, index, val) + { + struct ANASTASIS_ReduxAuthMethod *am = &b->authentication_methods[index]; + const char *type; + const char *instructions; + const char *mime_type = NULL; + void *challenge; + size_t challenge_size; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_string ("instructions", + &instructions), + GNUNET_JSON_spec_varsize ("challenge", + &challenge, + &challenge_size), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("mime_type", + &mime_type), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + check_fields (val, + "authentication_methods", + allowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + spec, + NULL, NULL)) + { + *detail = set_detail ("authentication_methods", + NULL); + return GNUNET_SYSERR; + } + am->type = GNUNET_strdup (type); + am->instructions = GNUNET_strdup (instructions); + am->mime_type = dup_or_null (mime_type); + am->challenge = GNUNET_memdup (challenge, + challenge_size); + am->challenge_size = challenge_size; + GNUNET_JSON_parse_free (spec); + } + return GNUNET_OK; +} + + +/** + * Parse the `policies` array. + * + * @param arr the array, may be NULL + * @param[out] b backup state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_policies (const json_t *arr, + struct ANASTASIS_ReduxBackup *b, + const char **detail) +{ + static const char *const pallowed[] = { + "methods", + "recovery_cost", + NULL + }; + static const char *const mallowed[] = { + "authentication_method", + "provider", + "truth", + NULL + }; + size_t index; + json_t *val; + + if (NULL == arr) + return GNUNET_OK; + b->have_policies = true; + b->policies_len = (unsigned int) json_array_size (arr); + b->policies = GNUNET_new_array (b->policies_len, + struct ANASTASIS_ReduxPolicy); + json_array_foreach (arr, index, val) + { + struct ANASTASIS_ReduxPolicy *p = &b->policies[index]; + const json_t *methods; + size_t mindex; + json_t *m; + bool no_cost; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("methods", + &methods), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("recovery_cost", + &p->recovery_cost), + &no_cost), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + check_fields (val, + "policies", + pallowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + spec, + NULL, NULL)) + { + *detail = set_detail ("policies", + "methods"); + return GNUNET_SYSERR; + } + p->have_recovery_cost = ! no_cost; + p->methods_len = (unsigned int) json_array_size (methods); + p->methods = GNUNET_new_array (p->methods_len, + struct ANASTASIS_ReduxPolicyMethod); + json_array_foreach (methods, mindex, m) + { + struct ANASTASIS_ReduxPolicyMethod *pm = &p->methods[mindex]; + const char *provider; + const json_t *truth = NULL; + struct GNUNET_JSON_Specification mspec[] = { + GNUNET_JSON_spec_uint32 ("authentication_method", + &pm->authentication_method.idx), + GNUNET_JSON_spec_string ("provider", + &provider), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("truth", + &truth), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + check_fields (m, + "policies.methods", + mallowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (m, + mspec, + NULL, NULL)) + { + *detail = set_detail ("policies.methods", + NULL); + return GNUNET_SYSERR; + } + ANASTASIS_REDUX_provider_url_set_ (&pm->provider, + provider); + if (NULL != truth) + { + uint32_t us; + struct GNUNET_JSON_Specification tspec[] = { + GNUNET_JSON_spec_uint32 ("upload_status", + &us), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (truth, + tspec, + NULL, NULL)) + { + *detail = set_detail ("policies.methods.truth", + "upload_status"); + return GNUNET_SYSERR; + } + pm->upload_status = (enum ANASTASIS_UploadStatus) us; + pm->truth = ANASTASIS_truth_from_json (truth); + if (NULL == pm->truth) + { + *detail = set_detail ("policies.methods", + "truth"); + return GNUNET_SYSERR; + } + } + } + } + return GNUNET_OK; +} + + +/** + * Parse the `policy_providers` array. + * + * @param arr the array, may be NULL + * @param[out] b backup state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_policy_providers (const json_t *arr, + struct ANASTASIS_ReduxBackup *b, + const char **detail) +{ + static const char *const allowed[] = { + "provider_url", + "payment_secret", + NULL + }; + size_t index; + json_t *val; + + if (NULL == arr) + return GNUNET_OK; + b->have_policy_providers = true; + b->policy_providers_len = (unsigned int) json_array_size (arr); + b->policy_providers = GNUNET_new_array (b->policy_providers_len, + struct + ANASTASIS_ReduxPolicyProvider); + json_array_foreach (arr, index, val) + { + struct ANASTASIS_ReduxPolicyProvider *pp = &b->policy_providers[index]; + const char *url; + bool no_ps; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("provider_url", + &url), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &pp->payment_secret), + &no_ps), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + check_fields (val, + "policy_providers", + allowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + spec, + NULL, NULL)) + { + *detail = set_detail ("policy_providers", + NULL); + return GNUNET_SYSERR; + } + ANASTASIS_REDUX_provider_url_set_ (&pp->provider_url, + url); + pp->have_payment_secret = ! no_ps; + } + return GNUNET_OK; +} + + +/** + * Parse the `success_details` object. + * + * @param obj the object, may be NULL + * @param[out] b backup state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_success_details (const json_t *obj, + struct ANASTASIS_ReduxBackup *b, + const char **detail) +{ + static const char *const allowed[] = { + "policy_version", + "policy_expiration", + NULL + }; + const char *url; + json_t *val; + unsigned int off = 0; + + if (NULL == obj) + return GNUNET_OK; + b->have_success_details = true; + b->success_details_len = (unsigned int) json_object_size (obj); + b->success_details = GNUNET_new_array (b->success_details_len, + struct ANASTASIS_ReduxSuccessDetail); + json_object_foreach ((json_t *) obj, url, val) + { + struct ANASTASIS_ReduxSuccessDetail *sd = &b->success_details[off]; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint32 ("policy_version", + &sd->policy_version), + GNUNET_JSON_spec_timestamp ("policy_expiration", + &sd->policy_expiration), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + check_fields (val, + "success_details", + allowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + spec, + NULL, NULL)) + { + *detail = set_detail ("success_details", + NULL); + return GNUNET_SYSERR; + } + ANASTASIS_REDUX_provider_url_set_ (&sd->provider_url, + url); + off++; + } + return GNUNET_OK; +} + + +/** + * Parse the backup-specific parts of @a json. + * + * @param json the state + * @param[out] b backup state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_backup (const json_t *json, + struct ANASTASIS_ReduxBackup *b, + const char **detail) +{ + const char *state_str; + const json_t *auth_methods = NULL; + const json_t *policies = NULL; + const json_t *policy_providers = NULL; + const json_t *upload_fees = NULL; + const json_t *payments = NULL; + const json_t *policy_payment_requests = NULL; + const json_t *success_details = NULL; + const json_t *core_secret = NULL; + const json_t *pay_arguments = NULL; + const char *secret_name = NULL; + bool no_expiration; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("backup_state", + &state_str), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("authentication_methods", + &auth_methods), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("policies", + &policies), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("policy_providers", + &policy_providers), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("upload_fees", + &upload_fees), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("payments", + &payments), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("policy_payment_requests", + &policy_payment_requests), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("success_details", + &success_details), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("core_secret", + &core_secret), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("pay_arguments", + &pay_arguments), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("secret_name", + &secret_name), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("expiration", + &b->expiration), + &no_expiration), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + *detail = set_detail ("state", + "backup_state"); + return GNUNET_SYSERR; + } + b->state = ANASTASIS_backup_state_from_string_ (state_str); + if (ANASTASIS_BACKUP_STATE_INVALID == b->state) + { + *detail = set_detail ("state", + "backup_state"); + return GNUNET_SYSERR; + } + b->have_expiration = ! no_expiration; + b->secret_name = dup_or_null (secret_name); + b->core_secret = json_incref ((json_t *) core_secret); + if ( (GNUNET_OK != + parse_auth_methods (auth_methods, + b, + detail)) || + (GNUNET_OK != + parse_policies (policies, + b, + detail)) || + (GNUNET_OK != + parse_policy_providers (policy_providers, + b, + detail)) || + (GNUNET_OK != + parse_success_details (success_details, + b, + detail)) ) + return GNUNET_SYSERR; + if (NULL != upload_fees) + { + static const char *const allowed[] = { + "fee", + NULL + }; + size_t index; + json_t *val; + + b->have_upload_fees = true; + b->upload_fees_len = (unsigned int) json_array_size (upload_fees); + b->upload_fees = GNUNET_new_array (b->upload_fees_len, + struct TALER_Amount); + json_array_foreach (upload_fees, index, val) + { + struct GNUNET_JSON_Specification fspec[] = { + TALER_JSON_spec_amount_any ("fee", + &b->upload_fees[index]), + GNUNET_JSON_spec_end () + }; + + if ( (GNUNET_OK != + check_fields (val, + "upload_fees", + allowed, + detail)) || + (GNUNET_OK != + GNUNET_JSON_parse (val, + fspec, + NULL, NULL)) ) + { + *detail = set_detail ("upload_fees", + "fee"); + return GNUNET_SYSERR; + } + } + } + if (NULL != payments) + { + size_t index; + json_t *val; + + b->have_payments = true; + b->payments_len = (unsigned int) json_array_size (payments); + b->payments = GNUNET_new_array (b->payments_len, + char *); + json_array_foreach (payments, index, val) + { + if (! json_is_string (val)) + { + *detail = set_detail ("payments", + NULL); + return GNUNET_SYSERR; + } + b->payments[index] = GNUNET_strdup (json_string_value (val)); + } + } + if (NULL != policy_payment_requests) + { + static const char *const allowed[] = { + "payto", + "provider", + NULL + }; + size_t index; + json_t *val; + + b->have_policy_payment_requests = true; + b->policy_payment_requests_len + = (unsigned int) json_array_size (policy_payment_requests); + b->policy_payment_requests + = GNUNET_new_array (b->policy_payment_requests_len, + struct ANASTASIS_ReduxPolicyPaymentRequest); + json_array_foreach (policy_payment_requests, index, val) + { + struct ANASTASIS_ReduxPolicyPaymentRequest *ppr + = &b->policy_payment_requests[index]; + const char *payto; + const char *provider; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_string ("payto", + &payto), + GNUNET_JSON_spec_string ("provider", + &provider), + GNUNET_JSON_spec_end () + }; + + if ( (GNUNET_OK != + check_fields (val, + "policy_payment_requests", + allowed, + detail)) || + (GNUNET_OK != + GNUNET_JSON_parse (val, + pspec, + NULL, NULL)) ) + { + *detail = set_detail ("policy_payment_requests", + NULL); + return GNUNET_SYSERR; + } + ppr->payto = GNUNET_strdup (payto); + ANASTASIS_REDUX_provider_url_set_ (&ppr->provider, + provider); + } + } + if (NULL != pay_arguments) + { + static const char *const allowed[] = { + "timeout", + NULL + }; + bool no_timeout; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &b->pay_arguments.timeout), + &no_timeout), + GNUNET_JSON_spec_end () + }; + + if ( (GNUNET_OK != + check_fields (pay_arguments, + "pay_arguments", + allowed, + detail)) || + (GNUNET_OK != + GNUNET_JSON_parse (pay_arguments, + pspec, + NULL, NULL)) ) + { + *detail = set_detail ("pay_arguments", + "timeout"); + return GNUNET_SYSERR; + } + b->have_pay_arguments = true; + b->pay_arguments.have_timeout = ! no_timeout; + } + return GNUNET_OK; +} + + +/** + * Parse the `recovery_information` object. + * + * @param obj the object + * @param[out] rr recovery state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_recovery_info (const json_t *obj, + struct ANASTASIS_ReduxRecovery *rr, + const char **detail) +{ + static const char *const allowed[] = { + "challenges", + "policies", + "secret_name", + "provider_url", + "version", + NULL + }; + static const char *const callowed[] = { + "uuid", + "uuid-display", + "type", + "instructions", + "payment_secret", + NULL + }; + static const char *const pallowed[] = { + "uuid", + NULL + }; + struct ANASTASIS_ReduxRecoveryInfo *ri; + const json_t *challenges; + const json_t *policies; + const char *secret_name = NULL; + const char *provider_url; + size_t index; + json_t *val; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("challenges", + &challenges), + GNUNET_JSON_spec_array_const ("policies", + &policies), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("secret_name", + &secret_name), + NULL), + GNUNET_JSON_spec_string ("provider_url", + &provider_url), + GNUNET_JSON_spec_end () + }; + + ri = GNUNET_new (struct ANASTASIS_ReduxRecoveryInfo); + rr->ri = ri; + if (GNUNET_OK != + check_fields (obj, + "recovery_information", + allowed, + detail)) + return GNUNET_SYSERR; + { + struct GNUNET_JSON_Specification vspec[] = { + GNUNET_JSON_spec_uint32 ("version", + &ri->version), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (obj, + vspec, + NULL, NULL)) + { + *detail = set_detail ("recovery_information", + "version"); + return GNUNET_SYSERR; + } + } + if (GNUNET_OK != + GNUNET_JSON_parse (obj, + spec, + NULL, NULL)) + { + *detail = set_detail ("recovery_information", + NULL); + return GNUNET_SYSERR; + } + ri->secret_name = dup_or_null (secret_name); + ANASTASIS_REDUX_provider_url_set_ (&ri->provider_url, + provider_url); + ri->challenges_len = (unsigned int) json_array_size (challenges); + ri->challenges = GNUNET_new_array (ri->challenges_len, + struct ANASTASIS_ReduxChallengeInfo); + json_array_foreach (challenges, index, val) + { + struct ANASTASIS_ReduxChallengeInfo *ci = &ri->challenges[index]; + const char *type; + const char *instructions; + bool no_ps; + struct GNUNET_JSON_Specification cspec[] = { + GNUNET_JSON_spec_fixed_auto ("uuid", + &ci->uuid), + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_string ("instructions", + &instructions), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &ci->payment_secret), + &no_ps), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + check_fields (val, + "recovery_information.challenges", + callowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + cspec, + NULL, NULL)) + { + *detail = set_detail ("recovery_information.challenges", + NULL); + return GNUNET_SYSERR; + } + ci->type = GNUNET_strdup (type); + ci->instructions = GNUNET_strdup (instructions); + ci->have_payment_secret = ! no_ps; + } + ri->policies_len = (unsigned int) json_array_size (policies); + ri->policies = GNUNET_new_array (ri->policies_len, + struct ANASTASIS_ReduxRecoveryPolicy); + json_array_foreach (policies, index, val) + { + struct ANASTASIS_ReduxRecoveryPolicy *p = &ri->policies[index]; + size_t uindex; + json_t *u; + + if (! json_is_array (val)) + { + *detail = set_detail ("recovery_information", + "policies"); + return GNUNET_SYSERR; + } + p->uuids_len = (unsigned int) json_array_size (val); + p->uuids = GNUNET_new_array (p->uuids_len, + struct ANASTASIS_CRYPTO_TruthUUIDP); + json_array_foreach (val, uindex, u) + { + struct GNUNET_JSON_Specification uspec[] = { + GNUNET_JSON_spec_fixed_auto ("uuid", + &p->uuids[uindex]), + GNUNET_JSON_spec_end () + }; + + if ( (GNUNET_OK != + check_fields (u, + "recovery_information.policies", + pallowed, + detail)) || + (GNUNET_OK != + GNUNET_JSON_parse (u, + uspec, + NULL, NULL)) ) + { + *detail = set_detail ("recovery_information.policies", + "uuid"); + return GNUNET_SYSERR; + } + } + } + return GNUNET_OK; +} + + +/** + * Parse one entry of the `challenge_feedback` object. + * + * @param val the feedback details + * @param[out] fb feedback entry to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_feedback_details (const json_t *val, + struct ANASTASIS_ReduxFeedback *fb, + const char **detail) +{ + static const char *const allowed[] = { + "state", + "display_hint", + "filename", + "address_hint", + "taler_pay_uri", + "provider", + "payment_secret", + "http_status", + "error_code", + "target_iban", + "target_business_name", + "wire_transfer_subject", + "challenge_amount", + "request_limit", + "request_frequency", + NULL + }; + const char *state; + const char *display_hint = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("state", + &state), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("display_hint", + &display_hint), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + check_fields (val, + "challenge_feedback", + allowed, + detail)) + return GNUNET_SYSERR; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + spec, + NULL, NULL)) + { + *detail = set_detail ("challenge_feedback", + "state"); + return GNUNET_SYSERR; + } + fb->display_hint = dup_or_null (display_hint); + if (0 == strcmp (state, + "code-in-file")) + { + const char *filename; + struct GNUNET_JSON_Specification dspec[] = { + GNUNET_JSON_spec_string ("filename", + &filename), + GNUNET_JSON_spec_end () + }; + + fb->status = ANASTASIS_RFS_CODE_IN_FILE; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + dspec, + NULL, NULL)) + { + *detail = set_detail ("challenge_feedback", + "filename"); + return GNUNET_SYSERR; + } + fb->details.code_in_file.filename = GNUNET_strdup (filename); + return GNUNET_OK; + } + if (0 == strcmp (state, + "send-to-address")) + { + const char *address_hint = NULL; + struct GNUNET_JSON_Specification dspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("address_hint", + &address_hint), + NULL), + GNUNET_JSON_spec_end () + }; + + fb->status = ANASTASIS_RFS_SEND_TO_ADDRESS; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + dspec, + NULL, NULL)) + { + *detail = set_detail ("challenge_feedback", + "address_hint"); + return GNUNET_SYSERR; + } + fb->details.send_to_address.address_hint = dup_or_null (address_hint); + return GNUNET_OK; + } + if (0 == strcmp (state, + "taler-payment")) + { + const char *uri; + const char *provider; + struct GNUNET_JSON_Specification dspec[] = { + GNUNET_JSON_spec_string ("taler_pay_uri", + &uri), + GNUNET_JSON_spec_string ("provider", + &provider), + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &fb->details.taler_payment.payment_secret), + GNUNET_JSON_spec_end () + }; + + fb->status = ANASTASIS_RFS_TALER_PAYMENT; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + dspec, + NULL, NULL)) + { + *detail = set_detail ("challenge_feedback", + "taler_pay_uri"); + return GNUNET_SYSERR; + } + fb->details.taler_payment.taler_pay_uri = GNUNET_strdup (uri); + ANASTASIS_REDUX_provider_url_set_ (&fb->details.taler_payment.provider, + provider); + return GNUNET_OK; + } + if ( (0 == strcmp (state, + "server-failure")) || + (0 == strcmp (state, + "truth-unknown")) ) + { + uint64_t ec; + struct GNUNET_JSON_Specification dspec[] = { + GNUNET_JSON_spec_uint64 ("error_code", + &ec), + GNUNET_JSON_spec_uint32 ("http_status", + &fb->details.server_failure.http_status), + GNUNET_JSON_spec_end () + }; + + fb->status = (0 == strcmp (state, + "server-failure")) + ? ANASTASIS_RFS_SERVER_FAILURE + : ANASTASIS_RFS_TRUTH_UNKNOWN; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + dspec, + NULL, NULL)) + { + *detail = set_detail ("challenge_feedback", + "error_code"); + return GNUNET_SYSERR; + } + fb->details.server_failure.ec = (enum TALER_ErrorCode) ec; + return GNUNET_OK; + } + if (0 == strcmp (state, + "iban-instructions")) + { + const char *iban; + const char *business_name; + const char *subject; + struct GNUNET_JSON_Specification dspec[] = { + GNUNET_JSON_spec_string ("target_iban", + &iban), + GNUNET_JSON_spec_string ("target_business_name", + &business_name), + GNUNET_JSON_spec_string ("wire_transfer_subject", + &subject), + TALER_JSON_spec_amount_any ("challenge_amount", + &fb->details.iban_instructions.amount), + GNUNET_JSON_spec_end () + }; + + fb->status = ANASTASIS_RFS_IBAN_INSTRUCTIONS; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + dspec, + NULL, NULL)) + { + *detail = set_detail ("challenge_feedback", + "target_iban"); + return GNUNET_SYSERR; + } + fb->details.iban_instructions.target_iban = GNUNET_strdup (iban); + fb->details.iban_instructions.target_business_name + = GNUNET_strdup (business_name); + fb->details.iban_instructions.wire_transfer_subject + = GNUNET_strdup (subject); + return GNUNET_OK; + } + if (0 == strcmp (state, + "solved")) + { + fb->status = ANASTASIS_RFS_SOLVED; + return GNUNET_OK; + } + if (0 == strcmp (state, + "incorrect-answer")) + { + uint64_t ec; + struct GNUNET_JSON_Specification dspec[] = { + GNUNET_JSON_spec_uint64 ("error_code", + &ec), + GNUNET_JSON_spec_end () + }; + + fb->status = ANASTASIS_RFS_INCORRECT_ANSWER; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + dspec, + NULL, NULL)) + { + *detail = set_detail ("challenge_feedback", + "error_code"); + return GNUNET_SYSERR; + } + fb->details.incorrect_answer.ec = (enum TALER_ErrorCode) ec; + return GNUNET_OK; + } + if (0 == strcmp (state, + "rate-limit-exceeded")) + { + uint64_t ec; + struct GNUNET_JSON_Specification dspec[] = { + GNUNET_JSON_spec_uint64 ("error_code", + &ec), + GNUNET_JSON_spec_uint64 ("request_limit", + &fb->details.rate_limit_exceeded.request_limit), + GNUNET_JSON_spec_relative_time ( + "request_frequency", + &fb->details.rate_limit_exceeded.request_frequency), + GNUNET_JSON_spec_end () + }; + + fb->status = ANASTASIS_RFS_RATE_LIMIT_EXCEEDED; + if (GNUNET_OK != + GNUNET_JSON_parse (val, + dspec, + NULL, NULL)) + { + *detail = set_detail ("challenge_feedback", + "request_limit"); + return GNUNET_SYSERR; + } + fb->details.rate_limit_exceeded.ec = (enum TALER_ErrorCode) ec; + return GNUNET_OK; + } + *detail = set_detail ("challenge_feedback", + "state"); + return GNUNET_SYSERR; +} + + +/** + * Parse the recovery-specific parts of @a json. + * + * @param json the state + * @param[out] rr recovery state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_recovery (const json_t *json, + struct ANASTASIS_ReduxRecovery *rr, + const char **detail) +{ + const char *state_str; + const json_t *recovery_information = NULL; + const json_t *recovery_document = NULL; + const json_t *challenge_feedback = NULL; + const json_t *core_secret = NULL; + bool no_selected; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("recovery_state", + &state_str), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("recovery_information", + &recovery_information), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("recovery_document", + &recovery_document), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("challenge_feedback", + &challenge_feedback), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("core_secret", + &core_secret), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("selected_challenge_uuid", + &rr->selected_challenge), + &no_selected), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + *detail = set_detail ("state", + "recovery_state"); + return GNUNET_SYSERR; + } + rr->state = ANASTASIS_recovery_state_from_string_ (state_str); + if (ANASTASIS_RECOVERY_STATE_INVALID == rr->state) + { + *detail = set_detail ("state", + "recovery_state"); + return GNUNET_SYSERR; + } + rr->have_selected_challenge = ! no_selected; + rr->core_secret = json_incref ((json_t *) core_secret); + if (NULL != recovery_information) + { + if (GNUNET_OK != + parse_recovery_info (recovery_information, + rr, + detail)) + return GNUNET_SYSERR; + } + if (NULL != recovery_document) + { + /* Hand the blob straight to src/lib: it owns that schema. The + handle comes back inert -- no callbacks, no network activity -- + until a handler calls ANASTASIS_recovery_resume(). */ + rr->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, + recovery_document); + if (NULL == rr->r) + { + *detail = set_detail ("state", + "recovery_document"); + return GNUNET_SYSERR; + } + } + if (NULL != challenge_feedback) + { + const char *uuid_str; + json_t *val; + unsigned int off = 0; + + rr->have_challenge_feedback = true; + rr->challenge_feedback_len + = (unsigned int) json_object_size (challenge_feedback); + rr->challenge_feedback + = GNUNET_new_array (rr->challenge_feedback_len, + struct ANASTASIS_ReduxFeedback); + json_object_foreach ((json_t *) challenge_feedback, uuid_str, val) + { + struct ANASTASIS_ReduxFeedback *fb = &rr->challenge_feedback[off]; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (uuid_str, + strlen (uuid_str), + &fb->uuid, + sizeof (fb->uuid))) + { + *detail = set_detail ("challenge_feedback", + uuid_str); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + parse_feedback_details (val, + fb, + detail)) + return GNUNET_SYSERR; + off++; + } + } + return GNUNET_OK; +} + + +/** + * Parse an `error` state. + * + * @param json the state + * @param[out] e error state to fill in + * @param[out] detail set to the offending field on failure + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_error (const json_t *json, + struct ANASTASIS_ReduxError *e, + const char **detail) +{ + static const char *const allowed[] = { + "reducer_type", + "code", + "hint", + "detail", + NULL + }; + uint64_t code; + const char *hint = NULL; + const char *dtl = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint64 ("code", + &code), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("hint", + &hint), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("detail", + &dtl), + NULL), + GNUNET_JSON_spec_end () + }; + + if ( (GNUNET_OK != + check_fields (json, + "state", + allowed, + detail)) || + (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) ) + { + *detail = set_detail ("state", + "code"); + return GNUNET_SYSERR; + } + e->code = (enum TALER_ErrorCode) code; + e->hint = dup_or_null (hint); + e->detail = dup_or_null (dtl); + return GNUNET_OK; +} + + +/** + * Field names permitted at the top level of a backup state. + */ +static const char *const backup_allowed[] = { + "reducer_type", + "backup_state", + "continents", + "countries", + "required_attributes", + "authentication_providers", + "identity_attributes", + "currencies", + "selected_continent", + "selected_country", + "authentication_methods", + "policies", + "policy_providers", + "upload_fees", + "payments", + "policy_payment_requests", + "success_details", + "core_secret", + "secret_name", + "expiration", + "pay_arguments", + NULL +}; + + +/** + * Field names permitted at the top level of a recovery state. + */ +static const char *const recovery_allowed[] = { + "reducer_type", + "recovery_state", + "continents", + "countries", + "required_attributes", + "authentication_providers", + "identity_attributes", + "currencies", + "selected_continent", + "selected_country", + "recovery_information", + "recovery_document", + "challenge_feedback", + "selected_challenge_uuid", + "core_secret", + NULL +}; + + +struct ANASTASIS_ReduxState * +ANASTASIS_REDUX_state_parse_ (const json_t *json, + enum TALER_ErrorCode *ec, + const char **detail) +{ + struct ANASTASIS_ReduxState *rs; + const char *type; + + *ec = TALER_EC_ANASTASIS_REDUCER_STATE_INVALID; + *detail = NULL; + if (! json_is_object (json)) + { + *detail = set_detail ("state", + NULL); + return NULL; + } + /* The reducer type is not always given explicitly: states written by + older versions (and the initial states of the CLI tests) are + identified by which of the two state fields is present. */ + type = json_string_value (json_object_get (json, + "reducer_type")); + if (NULL == type) + { + if (NULL != json_object_get (json, + "backup_state")) + type = "backup"; + else if (NULL != json_object_get (json, + "recovery_state")) + type = "recovery"; + else + { + *detail = set_detail ("state", + "reducer_type"); + return NULL; + } + } + rs = GNUNET_new (struct ANASTASIS_ReduxState); + if (0 == strcmp (type, + "backup")) + { + rs->type = ANASTASIS_RT_BACKUP; + if ( (GNUNET_OK != + check_fields (json, + "state", + backup_allowed, + detail)) || + (GNUNET_OK != + parse_common (json, + &rs->common, + detail)) || + (GNUNET_OK != + parse_backup (json, + &rs->details.backup, + detail)) ) + { + ANASTASIS_REDUX_state_free_ (rs); + return NULL; + } + return rs; + } + if (0 == strcmp (type, + "recovery")) + { + rs->type = ANASTASIS_RT_RECOVERY; + if ( (GNUNET_OK != + check_fields (json, + "state", + recovery_allowed, + detail)) || + (GNUNET_OK != + parse_common (json, + &rs->common, + detail)) || + (GNUNET_OK != + parse_recovery (json, + &rs->details.recovery, + detail)) ) + { + ANASTASIS_REDUX_state_free_ (rs); + return NULL; + } + return rs; + } + if (0 == strcmp (type, + "error")) + { + rs->type = ANASTASIS_RT_ERROR; + if (GNUNET_OK != + parse_error (json, + &rs->details.error, + detail)) + { + ANASTASIS_REDUX_state_free_ (rs); + return NULL; + } + return rs; + } + *detail = set_detail ("state", + "reducer_type"); + ANASTASIS_REDUX_state_free_ (rs); + return NULL; +} + + +/* end of anastasis_api_redux_parse.c */ diff --git a/src/reducer/anastasis_api_redux_serialize.c b/src/reducer/anastasis_api_redux_serialize.c @@ -0,0 +1,881 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 2021, 2022 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file reducer/anastasis_api_redux_serialize.c + * @brief write a reducer state back out as JSON + * @author Christian Grothoff + * + * The inverse of anastasis_api_redux_parse.c. Fields that are not set are + * omitted rather than serialized as null, matching what the JSON-based + * implementation this replaced produced. + */ +#include "platform.h" +#include "anastasis_redux.h" +#include "anastasis_api_redux.h" +#include "anastasis_api_redux_state.h" +#include <taler/taler_json_lib.h> + + +/** + * Add @a val to @a obj under @a key, unless @a val is NULL. + * + * @param[in,out] obj object to extend + * @param key field name + * @param[in] val value to add, reference is consumed + */ +static void +set_opt (json_t *obj, + const char *key, + json_t *val) +{ + if (NULL == val) + return; + GNUNET_assert (0 == + json_object_set_new (obj, + key, + val)); +} + + +/** + * Serialize the `continents` array. + * + * @param common state to serialize + * @return JSON array + */ +static json_t * +serialize_continents (const struct ANASTASIS_ReduxCommon *common) +{ + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + for (unsigned int i = 0; i < common->continents_len; i++) + { + const struct ANASTASIS_ReduxContinent *c = &common->continents[i]; + json_t *co; + + if (c->bare) + { + GNUNET_assert (0 == + json_array_append_new (arr, + json_string (c->name))); + continue; + } + co = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("name", + c->name)); + set_opt (co, + "name_i18n", + json_incref (c->name_i18n)); + GNUNET_assert (0 == + json_array_append_new (arr, + co)); + } + return arr; +} + + +/** + * Serialize the `countries` array. + * + * @param common state to serialize + * @return JSON array + */ +static json_t * +serialize_countries (const struct ANASTASIS_ReduxCommon *common) +{ + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + for (unsigned int i = 0; i < common->countries_len; i++) + { + const struct ANASTASIS_ReduxCountry *c = &common->countries[i]; + json_t *co; + + co = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("code", + c->code), + GNUNET_JSON_pack_string ("name", + c->name), + GNUNET_JSON_pack_string ("continent", + c->continent), + GNUNET_JSON_pack_conditional ( + NULL != c->call_code, + GNUNET_JSON_pack_string ("call_code", + c->call_code))); + set_opt (co, + "name_i18n", + json_incref (c->name_i18n)); + set_opt (co, + "continent_i18n", + json_incref (c->continent_i18n)); + if (NULL != c->currency) + set_opt (co, + "currency", + json_string (c->currency)); + GNUNET_assert (0 == + json_array_append_new (arr, + co)); + } + return arr; +} + + +/** + * Serialize the `required_attributes` array. + * + * @param common state to serialize + * @return JSON array + */ +static json_t * +serialize_required_attributes (const struct ANASTASIS_ReduxCommon *common) +{ + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + for (unsigned int i = 0; i < common->required_attributes_len; i++) + { + const struct ANASTASIS_ReduxAttributeSpec *a + = &common->required_attributes[i]; + json_t *ao; + + ao = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + a->type), + GNUNET_JSON_pack_string ("name", + a->name), + GNUNET_JSON_pack_string ("label", + a->label), + GNUNET_JSON_pack_string ("widget", + a->widget), + GNUNET_JSON_pack_conditional ( + NULL != a->uuid, + GNUNET_JSON_pack_string ("uuid", + a->uuid)), + GNUNET_JSON_pack_conditional ( + NULL != a->tooltip, + GNUNET_JSON_pack_string ("tooltip", + a->tooltip)), + GNUNET_JSON_pack_conditional ( + NULL != a->validation_regex, + GNUNET_JSON_pack_string ("validation-regex", + a->validation_regex)), + GNUNET_JSON_pack_conditional ( + NULL != a->validation_logic, + GNUNET_JSON_pack_string ("validation-logic", + a->validation_logic)), + GNUNET_JSON_pack_conditional ( + NULL != a->autocomplete, + GNUNET_JSON_pack_string ("autocomplete", + a->autocomplete)), + GNUNET_JSON_pack_conditional ( + a->have_optional, + GNUNET_JSON_pack_bool ("optional", + a->optional))); + set_opt (ao, + "label_i18n", + json_incref (a->label_i18n)); + GNUNET_assert (0 == + json_array_append_new (arr, + ao)); + } + return arr; +} + + +/** + * Serialize the `authentication_providers` object. + * + * @param common state to serialize + * @return JSON object + */ +static json_t * +serialize_providers (const struct ANASTASIS_ReduxCommon *common) +{ + json_t *obj; + + obj = json_object (); + GNUNET_assert (NULL != obj); + for (unsigned int i = 0; i < common->providers_len; i++) + { + const struct ANASTASIS_ReduxProvider *p = &common->providers[i]; + const char *status; + json_t *po; + + switch (p->status) + { + case ANASTASIS_RPS_NOT_CONTACTED: + status = "not-contacted"; + break; + case ANASTASIS_RPS_OK: + status = "ok"; + break; + case ANASTASIS_RPS_DISABLED: + status = "disabled"; + break; + case ANASTASIS_RPS_ERROR: + status = "error"; + break; + default: + GNUNET_assert (0); + } + if (ANASTASIS_RPS_ERROR == p->status) + { + po = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("status", + status), + GNUNET_JSON_pack_uint64 ("error_code", + p->error.ec), + GNUNET_JSON_pack_uint64 ("http_status", + p->error.http_status)); + } + else if (! p->have_config) + { + po = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("status", + status)); + } + else + { + const struct ANASTASIS_ReduxProviderConfig *cfg = &p->config; + json_t *methods; + + methods = json_array (); + GNUNET_assert (NULL != methods); + for (unsigned int j = 0; j < cfg->methods_len; j++) + { + const struct ANASTASIS_ReduxMethodSpec *ms = &cfg->methods[j]; + + GNUNET_assert (0 == + json_array_append_new ( + methods, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + ms->type), + TALER_JSON_pack_amount ("usage_fee", + &ms->usage_fee)))); + } + po = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("status", + status), + GNUNET_JSON_pack_array_steal ("methods", + methods), + TALER_JSON_pack_amount ("annual_fee", + &cfg->annual_fee), + TALER_JSON_pack_amount ("truth_upload_fee", + &cfg->truth_upload_fee), + TALER_JSON_pack_amount ("liability_limit", + &cfg->liability_limit), + GNUNET_JSON_pack_string ("business_name", + cfg->business_name), + GNUNET_JSON_pack_uint64 ("storage_limit_in_megabytes", + cfg->storage_limit_in_megabytes), + GNUNET_JSON_pack_data_auto ("provider_salt", + &cfg->provider_salt), + GNUNET_JSON_pack_uint64 ("http_status", + cfg->http_status), + GNUNET_JSON_pack_conditional ( + NULL != cfg->currency, + GNUNET_JSON_pack_string ("currency", + cfg->currency))); + } + GNUNET_assert (0 == + json_object_set_new (obj, + p->url.url, + po)); + } + return obj; +} + + +/** + * Add the fields shared by the backup and recovery variants to @a obj. + * + * @param[in,out] obj object to extend + * @param common state to serialize + */ +static void +serialize_common (json_t *obj, + const struct ANASTASIS_ReduxCommon *common) +{ + if (common->have_continents) + set_opt (obj, + "continents", + serialize_continents (common)); + if (common->have_countries) + set_opt (obj, + "countries", + serialize_countries (common)); + if (common->have_required_attributes) + set_opt (obj, + "required_attributes", + serialize_required_attributes (common)); + if (common->have_providers) + set_opt (obj, + "authentication_providers", + serialize_providers (common)); + if (NULL != common->identity_attributes) + set_opt (obj, + "identity_attributes", + json_incref (common->identity_attributes)); + if (common->have_currencies) + { + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + for (unsigned int i = 0; i < common->currencies_len; i++) + GNUNET_assert (0 == + json_array_append_new ( + arr, + json_string (common->currencies[i]))); + set_opt (obj, + "currencies", + arr); + } + if (NULL != common->selected_continent) + set_opt (obj, + "selected_continent", + json_string (common->selected_continent)); + if (NULL != common->selected_country) + set_opt (obj, + "selected_country", + json_string (common->selected_country)); +} + + +/** + * Serialize the backup-specific fields into @a obj. + * + * @param[in,out] obj object to extend + * @param b backup state to serialize + */ +static void +serialize_backup (json_t *obj, + const struct ANASTASIS_ReduxBackup *b) +{ + if (b->have_authentication_methods) + { + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + for (unsigned int i = 0; i < b->authentication_methods_len; i++) + { + const struct ANASTASIS_ReduxAuthMethod *am + = &b->authentication_methods[i]; + + GNUNET_assert (0 == + json_array_append_new ( + arr, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + am->type), + GNUNET_JSON_pack_string ("instructions", + am->instructions), + GNUNET_JSON_pack_data_varsize ("challenge", + am->challenge, + am->challenge_size), + GNUNET_JSON_pack_conditional ( + NULL != am->mime_type, + GNUNET_JSON_pack_string ("mime_type", + am->mime_type))))); + } + set_opt (obj, + "authentication_methods", + arr); + } + if (b->have_policies) + { + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + for (unsigned int i = 0; i < b->policies_len; i++) + { + const struct ANASTASIS_ReduxPolicy *p = &b->policies[i]; + json_t *methods; + + methods = json_array (); + GNUNET_assert (NULL != methods); + for (unsigned int j = 0; j < p->methods_len; j++) + { + const struct ANASTASIS_ReduxPolicyMethod *pm = &p->methods[j]; + json_t *mo; + + mo = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("authentication_method", + pm->authentication_method.idx), + GNUNET_JSON_pack_string ("provider", + pm->provider.url)); + if (NULL != pm->truth) + { + json_t *truth = ANASTASIS_truth_to_json (pm->truth); + + GNUNET_assert (NULL != truth); + GNUNET_assert (0 == + json_object_set_new (truth, + "upload_status", + json_integer ( + pm->upload_status))); + GNUNET_assert (0 == + json_object_set_new (mo, + "truth", + truth)); + } + GNUNET_assert (0 == + json_array_append_new (methods, + mo)); + } + GNUNET_assert (0 == + json_array_append_new ( + arr, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_conditional ( + p->have_recovery_cost, + TALER_JSON_pack_amount ("recovery_cost", + &p->recovery_cost)), + GNUNET_JSON_pack_array_steal ("methods", + methods)))); + } + set_opt (obj, + "policies", + arr); + } + if (b->have_policy_providers) + { + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + for (unsigned int i = 0; i < b->policy_providers_len; i++) + { + const struct ANASTASIS_ReduxPolicyProvider *pp + = &b->policy_providers[i]; + + GNUNET_assert (0 == + json_array_append_new ( + arr, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("provider_url", + pp->provider_url.url), + GNUNET_JSON_pack_conditional ( + pp->have_payment_secret, + GNUNET_JSON_pack_data_auto ( + "payment_secret", + &pp->payment_secret))))); + } + set_opt (obj, + "policy_providers", + arr); + } + if (b->have_upload_fees) + { + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + for (unsigned int i = 0; i < b->upload_fees_len; i++) + GNUNET_assert (0 == + json_array_append_new ( + arr, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("fee", + &b->upload_fees[i])))); + set_opt (obj, + "upload_fees", + arr); + } + if (b->have_payments) + { + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + for (unsigned int i = 0; i < b->payments_len; i++) + GNUNET_assert (0 == + json_array_append_new (arr, + json_string (b->payments[i]))); + set_opt (obj, + "payments", + arr); + } + if (b->have_policy_payment_requests) + { + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + for (unsigned int i = 0; i < b->policy_payment_requests_len; i++) + { + const struct ANASTASIS_ReduxPolicyPaymentRequest *ppr + = &b->policy_payment_requests[i]; + + GNUNET_assert (0 == + json_array_append_new ( + arr, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("payto", + ppr->payto), + GNUNET_JSON_pack_string ("provider", + ppr->provider.url)))); + } + set_opt (obj, + "policy_payment_requests", + arr); + } + if (b->have_success_details) + { + json_t *sd; + + sd = json_object (); + GNUNET_assert (NULL != sd); + for (unsigned int i = 0; i < b->success_details_len; i++) + { + const struct ANASTASIS_ReduxSuccessDetail *d = &b->success_details[i]; + + GNUNET_assert (0 == + json_object_set_new ( + sd, + d->provider_url.url, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("policy_version", + d->policy_version), + GNUNET_JSON_pack_timestamp ("policy_expiration", + d->policy_expiration)))); + } + set_opt (obj, + "success_details", + sd); + } + if (NULL != b->core_secret) + set_opt (obj, + "core_secret", + json_incref (b->core_secret)); + if (NULL != b->secret_name) + set_opt (obj, + "secret_name", + json_string (b->secret_name)); + if (b->have_expiration) + set_opt (obj, + "expiration", + GNUNET_JSON_from_timestamp (b->expiration)); + if (b->have_pay_arguments) + { + json_t *pa; + + pa = json_object (); + GNUNET_assert (NULL != pa); + if (b->pay_arguments.have_timeout) + set_opt (pa, + "timeout", + GNUNET_JSON_from_time_rel (b->pay_arguments.timeout)); + set_opt (obj, + "pay_arguments", + pa); + } +} + + +/** + * Serialize the recovery information @a ri. + * + * @param ri recovery information to serialize + * @return JSON object + */ +static json_t * +serialize_recovery_info (const struct ANASTASIS_ReduxRecoveryInfo *ri) +{ + json_t *challenges; + json_t *policies; + + challenges = json_array (); + GNUNET_assert (NULL != challenges); + for (unsigned int i = 0; i < ri->challenges_len; i++) + { + const struct ANASTASIS_ReduxChallengeInfo *ci = &ri->challenges[i]; + + GNUNET_assert (0 == + json_array_append_new ( + challenges, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("uuid", + &ci->uuid), + GNUNET_JSON_pack_string ("type", + ci->type), + GNUNET_JSON_pack_string ("uuid-display", + ANASTASIS_CRYPTO_uuid2s ( + &ci->uuid)), + GNUNET_JSON_pack_string ("instructions", + ci->instructions), + GNUNET_JSON_pack_conditional ( + ci->have_payment_secret, + GNUNET_JSON_pack_data_auto ("payment_secret", + &ci->payment_secret))))); + } + policies = json_array (); + GNUNET_assert (NULL != policies); + for (unsigned int i = 0; i < ri->policies_len; i++) + { + const struct ANASTASIS_ReduxRecoveryPolicy *p = &ri->policies[i]; + json_t *pa; + + pa = json_array (); + GNUNET_assert (NULL != pa); + for (unsigned int j = 0; j < p->uuids_len; j++) + GNUNET_assert (0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("uuid", + &p->uuids[j])))); + GNUNET_assert (0 == + json_array_append_new (policies, + pa)); + } + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("challenges", + challenges), + GNUNET_JSON_pack_array_steal ("policies", + policies), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("secret_name", + ri->secret_name)), + GNUNET_JSON_pack_string ("provider_url", + ri->provider_url.url), + GNUNET_JSON_pack_uint64 ("version", + ri->version)); +} + + +/** + * Serialize one challenge feedback entry. + * + * @param fb feedback to serialize + * @return JSON object + */ +static json_t * +serialize_feedback (const struct ANASTASIS_ReduxFeedback *fb) +{ + json_t *fo; + + switch (fb->status) + { + case ANASTASIS_RFS_CODE_IN_FILE: + fo = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("state", + "code-in-file"), + GNUNET_JSON_pack_string ("filename", + fb->details.code_in_file.filename)); + break; + case ANASTASIS_RFS_SEND_TO_ADDRESS: + fo = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("state", + "send-to-address"), + GNUNET_JSON_pack_conditional ( + NULL != fb->details.send_to_address.address_hint, + GNUNET_JSON_pack_string ("address_hint", + fb->details.send_to_address.address_hint))); + break; + case ANASTASIS_RFS_TALER_PAYMENT: + fo = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("state", + "taler-payment"), + GNUNET_JSON_pack_string ("taler_pay_uri", + fb->details.taler_payment.taler_pay_uri), + GNUNET_JSON_pack_string ("provider", + fb->details.taler_payment.provider.url), + GNUNET_JSON_pack_data_auto ("payment_secret", + &fb->details.taler_payment.payment_secret)); + break; + case ANASTASIS_RFS_SERVER_FAILURE: + fo = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("state", + "server-failure"), + GNUNET_JSON_pack_uint64 ("http_status", + fb->details.server_failure.http_status), + GNUNET_JSON_pack_uint64 ("error_code", + fb->details.server_failure.ec)); + break; + case ANASTASIS_RFS_TRUTH_UNKNOWN: + fo = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("state", + "truth-unknown"), + GNUNET_JSON_pack_uint64 ("http_status", + fb->details.server_failure.http_status), + GNUNET_JSON_pack_uint64 ("error_code", + fb->details.server_failure.ec)); + break; + case ANASTASIS_RFS_IBAN_INSTRUCTIONS: + fo = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("state", + "iban-instructions"), + GNUNET_JSON_pack_string ("target_iban", + fb->details.iban_instructions.target_iban), + GNUNET_JSON_pack_string ( + "target_business_name", + fb->details.iban_instructions.target_business_name), + GNUNET_JSON_pack_string ( + "wire_transfer_subject", + fb->details.iban_instructions.wire_transfer_subject), + TALER_JSON_pack_amount ("challenge_amount", + &fb->details.iban_instructions.amount)); + break; + case ANASTASIS_RFS_SOLVED: + fo = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("state", + "solved")); + break; + case ANASTASIS_RFS_INCORRECT_ANSWER: + fo = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("state", + "incorrect-answer"), + GNUNET_JSON_pack_uint64 ("error_code", + fb->details.incorrect_answer.ec)); + break; + case ANASTASIS_RFS_RATE_LIMIT_EXCEEDED: + fo = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("state", + "rate-limit-exceeded"), + GNUNET_JSON_pack_uint64 ( + "request_limit", + fb->details.rate_limit_exceeded.request_limit), + GNUNET_JSON_pack_time_rel ( + "request_frequency", + fb->details.rate_limit_exceeded.request_frequency), + GNUNET_JSON_pack_uint64 ("error_code", + fb->details.rate_limit_exceeded.ec)); + break; + default: + GNUNET_assert (0); + } + if (NULL != fb->display_hint) + set_opt (fo, + "display_hint", + json_string (fb->display_hint)); + return fo; +} + + +/** + * Serialize the recovery-specific fields into @a obj. + * + * @param[in,out] obj object to extend + * @param rr recovery state to serialize + */ +static void +serialize_recovery (json_t *obj, + const struct ANASTASIS_ReduxRecovery *rr) +{ + if (NULL != rr->ri) + set_opt (obj, + "recovery_information", + serialize_recovery_info (rr->ri)); + if (NULL != rr->r) + set_opt (obj, + "recovery_document", + ANASTASIS_recovery_serialize (rr->r)); + if (rr->have_challenge_feedback) + { + json_t *cf; + + cf = json_object (); + GNUNET_assert (NULL != cf); + for (unsigned int i = 0; i < rr->challenge_feedback_len; i++) + { + const struct ANASTASIS_ReduxFeedback *fb = &rr->challenge_feedback[i]; + char uuid[sizeof (fb->uuid) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string (&fb->uuid, + sizeof (fb->uuid), + uuid, + sizeof (uuid)); + GNUNET_assert (NULL != end); + *end = '\0'; + GNUNET_assert (0 == + json_object_set_new (cf, + uuid, + serialize_feedback (fb))); + } + set_opt (obj, + "challenge_feedback", + cf); + } + if (rr->have_selected_challenge) + set_opt (obj, + "selected_challenge_uuid", + GNUNET_JSON_from_data_auto (&rr->selected_challenge)); + if (NULL != rr->core_secret) + set_opt (obj, + "core_secret", + json_incref (rr->core_secret)); +} + + +json_t * +ANASTASIS_REDUX_state_serialize_ (const struct ANASTASIS_ReduxState *rs) +{ + json_t *obj; + + switch (rs->type) + { + case ANASTASIS_RT_BACKUP: + obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("reducer_type", + "backup"), + GNUNET_JSON_pack_string ("backup_state", + ANASTASIS_backup_state_to_string_ ( + rs->details.backup.state))); + serialize_common (obj, + &rs->common); + serialize_backup (obj, + &rs->details.backup); + return obj; + case ANASTASIS_RT_RECOVERY: + obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("reducer_type", + "recovery"), + GNUNET_JSON_pack_string ("recovery_state", + ANASTASIS_recovery_state_to_string_ ( + rs->details.recovery.state))); + serialize_common (obj, + &rs->common); + serialize_recovery (obj, + &rs->details.recovery); + return obj; + case ANASTASIS_RT_ERROR: + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("reducer_type", + "error"), + GNUNET_JSON_pack_uint64 ("code", + rs->details.error.code), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("hint", + rs->details.error.hint)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("detail", + rs->details.error.detail))); + } + GNUNET_assert (0); + return NULL; +} + + +/* end of anastasis_api_redux_serialize.c */ diff --git a/src/reducer/anastasis_api_redux_state.c b/src/reducer/anastasis_api_redux_state.c @@ -0,0 +1,147 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 2021, 2022 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file reducer/anastasis_api_redux_state.c + * @brief lookup helpers for the typed reducer state + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_redux.h" +#include "anastasis_api_redux.h" +#include "anastasis_api_redux_state.h" + + +void +ANASTASIS_REDUX_provider_url_set_ (struct ANASTASIS_ProviderUrl *pu, + const char *url) +{ + GNUNET_free (pu->url); + pu->url = GNUNET_strdup (url); +} + + +struct ANASTASIS_ReduxProvider * +ANASTASIS_REDUX_provider_find_ (const struct ANASTASIS_ReduxCommon *common, + const char *url) +{ + for (unsigned int i = 0; i < common->providers_len; i++) + if (0 == strcmp (common->providers[i].url.url, + url)) + return &common->providers[i]; + return NULL; +} + + +struct ANASTASIS_ReduxProvider * +ANASTASIS_REDUX_provider_get_ (struct ANASTASIS_ReduxCommon *common, + const char *url) +{ + struct ANASTASIS_ReduxProvider *p; + unsigned int off; + + p = ANASTASIS_REDUX_provider_find_ (common, + url); + if (NULL != p) + return p; + off = common->providers_len; + GNUNET_array_append (common->providers, + common->providers_len, + (struct ANASTASIS_ReduxProvider) { + .status = ANASTASIS_RPS_NOT_CONTACTED + }); + common->have_providers = true; + p = &common->providers[off]; + ANASTASIS_REDUX_provider_url_set_ (&p->url, + url); + return p; +} + + +struct ANASTASIS_ReduxFeedback * +ANASTASIS_REDUX_feedback_find_ (const struct ANASTASIS_ReduxRecovery *rr, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid) +{ + for (unsigned int i = 0; i < rr->challenge_feedback_len; i++) + if (0 == + GNUNET_memcmp (&rr->challenge_feedback[i].uuid, + uuid)) + return &rr->challenge_feedback[i]; + return NULL; +} + + +struct ANASTASIS_ReduxFeedback * +ANASTASIS_REDUX_feedback_get_ (struct ANASTASIS_ReduxRecovery *rr, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid) +{ + struct ANASTASIS_ReduxFeedback *fb; + unsigned int off; + + fb = ANASTASIS_REDUX_feedback_find_ (rr, + uuid); + if (NULL != fb) + { + /* Overwriting an earlier verdict: drop whatever the old variant + owned so the caller can fill in a fresh one. */ + ANASTASIS_REDUX_feedback_clear_ (fb); + return fb; + } + off = rr->challenge_feedback_len; + GNUNET_array_append (rr->challenge_feedback, + rr->challenge_feedback_len, + (struct ANASTASIS_ReduxFeedback) { + .uuid = *uuid + }); + rr->have_challenge_feedback = true; + return &rr->challenge_feedback[off]; +} + + +struct ANASTASIS_ReduxChallengeInfo * +ANASTASIS_REDUX_challenge_find_ (const struct ANASTASIS_ReduxRecovery *rr, + const struct + ANASTASIS_CRYPTO_TruthUUIDP *uuid) +{ + if (NULL == rr->ri) + return NULL; + for (unsigned int i = 0; i < rr->ri->challenges_len; i++) + if (0 == + GNUNET_memcmp (&rr->ri->challenges[i].uuid, + uuid)) + return &rr->ri->challenges[i]; + return NULL; +} + + +void +ANASTASIS_REDUX_return_ (struct ANASTASIS_ReduxState *rs, + ANASTASIS_ActionCallback cb, + void *cb_cls, + enum TALER_ErrorCode ec) +{ + json_t *json; + + json = ANASTASIS_REDUX_state_serialize_ (rs); + GNUNET_assert (NULL != json); + cb (cb_cls, + ec, + json); + json_decref (json); + ANASTASIS_REDUX_state_free_ (rs); +} + + +/* end of anastasis_api_redux_state.c */ diff --git a/src/reducer/anastasis_api_redux_state.h b/src/reducer/anastasis_api_redux_state.h @@ -0,0 +1,1446 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 2021, 2022 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file reducer/anastasis_api_redux_state.h + * @brief typed representation of the reducer state + * @author Christian Grothoff + * + * The reducer's public API (`anastasis_redux.h`) speaks JSON, but the reducer + * logic itself does not: every entry point parses the incoming JSON into a + * `struct ANASTASIS_ReduxState` exactly once, operates on that, and serializes + * it back exactly once on the way out. The three generic routines at the + * bottom of this file are the only places that know the JSON encoding. + * + * Parsing is strict: a field that is not described here is an error + * (#TALER_EC_ANASTASIS_REDUCER_STATE_INVALID). There are two deliberate + * exceptions, both marked `json_t *` below, where the exact bytes the user + * supplied are cryptographically load-bearing and must not be normalized: + * @e identity_attributes (PoW-hashed to derive the account key) and + * @e core_secret (its serialization *is* the uploaded secret). + */ +#ifndef ANASTASIS_API_REDUX_STATE_H +#define ANASTASIS_API_REDUX_STATE_H + +#include "anastasis_redux.h" +#include "anastasis_api_redux.h" + + +/* ************************ Wrapped primitives *********************** */ + +/** + * URL of an Anastasis provider. Wrapped so that it cannot be confused + * with the many other strings floating around the reducer (business + * names, payto URIs, display hints, ...). + */ +struct ANASTASIS_ProviderUrl +{ + /** + * The URL itself. Owned by the containing structure. + */ + char *url; +}; + + +/** + * Index into the @e policies array of a backup state. + */ +struct ANASTASIS_PolicyIndex +{ + /** + * The index. + */ + unsigned int idx; +}; + + +/** + * Index into the @e methods array of a policy, or into the + * @e authentication_methods array of a backup state. + */ +struct ANASTASIS_MethodIndex +{ + /** + * The index. + */ + unsigned int idx; +}; + + +/** + * Bitmask selecting which optional identity attributes were used. + */ +struct ANASTASIS_AttributeMask +{ + /** + * The mask. + */ + uint64_t mask; +}; + + +/* ************************ Geography ******************************** */ + +/** + * A continent the user may select. + */ +struct ANASTASIS_ReduxContinent +{ + /** + * Name of the continent, e.g. "Europe". + */ + char *name; + + /** + * Translations of @e name, NULL if not given. An object mapping + * language tags to strings, kept verbatim: the reducer only carries + * translations from the resource files to the user interface and + * never interprets them. + */ + json_t *name_i18n; + + /** + * True if this continent was given as a bare string rather than as + * an object. Both forms occur: the reducer itself emits objects, + * but hand-written states use plain names. + */ + bool bare; +}; + + +/** + * A country the user may select. Copied verbatim from + * `contrib/redux.countries.json`. + */ +struct ANASTASIS_ReduxCountry +{ + /** + * Two-letter country code, e.g. "ch". + */ + char *code; + + /** + * Name of the country. + */ + char *name; + + /** + * Continent the country is on. + */ + char *continent; + + /** + * International dialling prefix, e.g. "+41". + */ + char *call_code; + + /** + * Translations of @e name, NULL if not given. Kept verbatim, see + * #ANASTASIS_ReduxContinent. + */ + json_t *name_i18n; + + /** + * Translations of @e continent, NULL if not given. Kept verbatim, + * see #ANASTASIS_ReduxContinent. + */ + json_t *continent_i18n; + + /** + * Currency used in this country, NULL if not given. Carried through + * for the benefit of user interfaces; the reducer does not use it. + */ + char *currency; +}; + + +/** + * Specification of one identity attribute we ask the user for. + * Copied verbatim from `contrib/redux.$CC.json`. + */ +struct ANASTASIS_ReduxAttributeSpec +{ + /** + * Type of the attribute, e.g. "string" or "date". + */ + char *type; + + /** + * Name of the attribute; the key under which the user's answer + * appears in @e identity_attributes. + */ + char *name; + + /** + * Human-readable label. + */ + char *label; + + /** + * Widget the GTK UI should use to ask for this attribute. + */ + char *widget; + + /** + * UUID identifying this attribute. + */ + char *uuid; + + /** + * Tooltip for the UI, NULL if not given. + */ + char *tooltip; + + /** + * Regular expression the value must match, NULL if not given. + */ + char *validation_regex; + + /** + * Name of a validation function to `dlsym()`, NULL if not given. + */ + char *validation_logic; + + /** + * Input mask for autocompletion, NULL if not given. + */ + char *autocomplete; + + /** + * Translations of @e label, NULL if not given. Kept verbatim, see + * #ANASTASIS_ReduxContinent. + */ + json_t *label_i18n; + + /** + * True if the user may leave this attribute blank. + */ + bool optional; + + /** + * True if @e optional was explicitly present in the JSON. Needed so + * that serialization does not invent an `"optional": false` field + * where the resource file had none. + */ + bool have_optional; +}; + + +/* ************************ Providers ******************************** */ + +/** + * Status of an Anastasis provider in the reducer state. + */ +enum ANASTASIS_ReduxProviderStatus +{ + /** + * We have not (yet) talked to this provider. + */ + ANASTASIS_RPS_NOT_CONTACTED = 0, + + /** + * We obtained the provider's /config successfully. + */ + ANASTASIS_RPS_OK = 1, + + /** + * The provider was taken out of consideration, typically because an + * upload to it failed. Note that any configuration we obtained + * earlier is retained (and re-serialized), matching the behaviour of + * the JSON-based implementation this replaced. + */ + ANASTASIS_RPS_DISABLED = 2, + + /** + * Talking to the provider failed. + */ + ANASTASIS_RPS_ERROR = 3 +}; + + +/** + * One authentication method offered by a provider, with its price. + */ +struct ANASTASIS_ReduxMethodSpec +{ + /** + * Type of the method, e.g. "question" or "sms". + */ + char *type; + + /** + * Fee charged for using this method during recovery. + */ + struct TALER_Amount usage_fee; +}; + + +/** + * Configuration of a provider, as obtained from its /config endpoint. + */ +struct ANASTASIS_ReduxProviderConfig +{ + /** + * Methods the provider supports. + */ + struct ANASTASIS_ReduxMethodSpec *methods; + + /** + * Length of the @e methods array. + */ + unsigned int methods_len; + + /** + * Fee for storing a policy for a year. + */ + struct TALER_Amount annual_fee; + + /** + * Fee for uploading one truth. + */ + struct TALER_Amount truth_upload_fee; + + /** + * Maximum liability the provider accepts for data loss. + */ + struct TALER_Amount liability_limit; + + /** + * Salt of this provider. + */ + struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt; + + /** + * Business name of the provider. + */ + char *business_name; + + /** + * Currency the provider charges in, NULL if not given. Carried + * through for the benefit of user interfaces. + */ + char *currency; + + /** + * Maximum upload size the provider accepts, in megabytes. + */ + uint32_t storage_limit_in_megabytes; + + /** + * HTTP status of the /config request. + */ + uint32_t http_status; +}; + + +/** + * Why we could not use a provider. + */ +struct ANASTASIS_ReduxProviderError +{ + /** + * Error code of the failed request. + */ + enum TALER_ErrorCode ec; + + /** + * HTTP status of the failed request, 0 if we never got one. + */ + uint32_t http_status; +}; + + +/** + * An Anastasis provider we know about. Serializes as one entry of the + * `authentication_providers` object, keyed by @e url. + */ +struct ANASTASIS_ReduxProvider +{ + /** + * URL of the provider; the key this entry is stored under. + */ + struct ANASTASIS_ProviderUrl url; + + /** + * What we know about this provider. + */ + enum ANASTASIS_ReduxProviderStatus status; + + /** + * True if @e config is valid. Set whenever a /config reply was + * obtained, and *not* cleared when @e status becomes + * #ANASTASIS_RPS_DISABLED. + */ + bool have_config; + + /** + * Configuration of the provider; valid iff @e have_config. + */ + struct ANASTASIS_ReduxProviderConfig config; + + /** + * Failure details; valid iff @e status is #ANASTASIS_RPS_ERROR. + */ + struct ANASTASIS_ReduxProviderError error; +}; + + +/* ******************** Backup-specific state ************************ */ + +/** + * An authentication method the user configured. + */ +struct ANASTASIS_ReduxAuthMethod +{ + /** + * Type of the method, e.g. "question". + */ + char *type; + + /** + * Instructions to show the user during recovery. + */ + char *instructions; + + /** + * The challenge itself (answer, phone number, e-mail address, ...). + */ + void *challenge; + + /** + * Number of bytes in @e challenge. + */ + size_t challenge_size; + + /** + * MIME type of @e challenge, NULL if not given. + */ + char *mime_type; +}; + + +/** + * One member of a policy: authenticate with method + * @e authentication_method at provider @e provider. + */ +struct ANASTASIS_ReduxPolicyMethod +{ + /** + * Index into the backup state's @e authentication_methods array. + */ + struct ANASTASIS_MethodIndex authentication_method; + + /** + * Provider that stores the key share for this method. + */ + struct ANASTASIS_ProviderUrl provider; + + /** + * Truth uploaded for this method, NULL until the upload happened. + * Owned by this structure; freed with ANASTASIS_truth_free(). + */ + struct ANASTASIS_Truth *truth; + + /** + * Status of the upload of @e truth; only meaningful if @e truth is + * non-NULL. + */ + enum ANASTASIS_UploadStatus upload_status; +}; + + +/** + * A recovery policy: a set of methods that together recover the secret. + */ +struct ANASTASIS_ReduxPolicy +{ + /** + * Methods that make up this policy. + */ + struct ANASTASIS_ReduxPolicyMethod *methods; + + /** + * Length of the @e methods array. + */ + unsigned int methods_len; + + /** + * What recovering via this policy would cost; only valid if + * @e have_recovery_cost. Carried through for user interfaces. + */ + struct TALER_Amount recovery_cost; + + /** + * True if @e recovery_cost was given. + */ + bool have_recovery_cost; +}; + + +/** + * A provider we intend to store the recovery document at. + */ +struct ANASTASIS_ReduxPolicyProvider +{ + /** + * URL of the provider. + */ + struct ANASTASIS_ProviderUrl provider_url; + + /** + * Payment secret remembered from an earlier payment request; only + * valid if @e have_payment_secret. + */ + struct ANASTASIS_PaymentSecretP payment_secret; + + /** + * True if @e payment_secret is set. + */ + bool have_payment_secret; +}; + + +/** + * Result of a successful policy upload at one provider. + */ +struct ANASTASIS_ReduxSuccessDetail +{ + /** + * URL of the provider; the key this entry is stored under. + */ + struct ANASTASIS_ProviderUrl provider_url; + + /** + * When the stored policy expires. + */ + struct GNUNET_TIME_Timestamp policy_expiration; + + /** + * Version the policy was stored under. + */ + uint32_t policy_version; +}; + + +/** + * A payment we are asking the application to make for a policy upload. + */ +struct ANASTASIS_ReduxPolicyPaymentRequest +{ + /** + * Payment request URI. + */ + char *payto; + + /** + * Provider that wants to be paid. + */ + struct ANASTASIS_ProviderUrl provider; +}; + + +/** + * Arguments the application passed to the most recent "pay" action. + */ +struct ANASTASIS_ReduxPayArguments +{ + /** + * How long to wait for the payment to go through. + */ + struct GNUNET_TIME_Relative timeout; + + /** + * True if @e timeout was given. + */ + bool have_timeout; +}; + + +/** + * The `backup` variant of the reducer state. + */ +struct ANASTASIS_ReduxBackup +{ + /** + * Which step of the backup process we are at. + */ + enum ANASTASIS_BackupState state; + + /** + * Authentication methods the user configured. + */ + struct ANASTASIS_ReduxAuthMethod *authentication_methods; + + /** + * Length of the @e authentication_methods array. + */ + unsigned int authentication_methods_len; + + /** + * Policies over the @e authentication_methods. + */ + struct ANASTASIS_ReduxPolicy *policies; + + /** + * Length of the @e policies array. + */ + unsigned int policies_len; + + /** + * Providers we will store the recovery document at. + */ + struct ANASTASIS_ReduxPolicyProvider *policy_providers; + + /** + * Length of the @e policy_providers array. + */ + unsigned int policy_providers_len; + + /** + * Per-currency total of what the backup will cost. + */ + struct TALER_Amount *upload_fees; + + /** + * Length of the @e upload_fees array. + */ + unsigned int upload_fees_len; + + /** + * Taler pay URIs the application must settle before we can continue + * uploading truths. + */ + char **payments; + + /** + * Length of the @e payments array. + */ + unsigned int payments_len; + + /** + * Payments the application must settle before we can upload the + * recovery document. + */ + struct ANASTASIS_ReduxPolicyPaymentRequest *policy_payment_requests; + + /** + * Length of the @e policy_payment_requests array. + */ + unsigned int policy_payment_requests_len; + + /** + * Where the recovery document ended up, per provider. + */ + struct ANASTASIS_ReduxSuccessDetail *success_details; + + /** + * Length of the @e success_details array. + */ + unsigned int success_details_len; + + /** + * The secret to back up. Deliberately untyped: its serialization is + * literally the secret that gets uploaded, so the reducer must never + * normalize it. NULL if not (yet) set. + */ + json_t *core_secret; + + /** + * Name the user gave the secret, NULL if not set. + */ + char *secret_name; + + /** + * When the backup should expire. + */ + struct GNUNET_TIME_Timestamp expiration; + + /** + * True if @e expiration is set. + */ + bool have_expiration; + + /** + * Arguments of the last "pay" action. + */ + struct ANASTASIS_ReduxPayArguments pay_arguments; + + /** + * True if @e pay_arguments is set. + */ + bool have_pay_arguments; + + /** + * True if the state has an @e authentication_methods field at all. + * As in #ANASTASIS_ReduxCommon, presence is tracked separately from + * the length because an empty array is meaningful. + */ + bool have_authentication_methods; + + /** + * True if the state has a @e policies field at all. + */ + bool have_policies; + + /** + * True if the state has a @e policy_providers field at all. + */ + bool have_policy_providers; + + /** + * True if the state has an @e upload_fees field at all. + */ + bool have_upload_fees; + + /** + * True if the state has a @e payments field at all. + */ + bool have_payments; + + /** + * True if the state has a @e policy_payment_requests field at all. + */ + bool have_policy_payment_requests; + + /** + * True if the state has a @e success_details field at all. + */ + bool have_success_details; +}; + + +/* ******************* Recovery-specific state *********************** */ + +/** + * A challenge the user could solve to recover a key share. + */ +struct ANASTASIS_ReduxChallengeInfo +{ + /** + * UUID of the truth behind this challenge. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + + /** + * Type of the challenge, e.g. "question". + */ + char *type; + + /** + * Instructions to show the user. + */ + char *instructions; + + /** + * Payment secret remembered from a payment request for this + * challenge; only valid if @e have_payment_secret. + */ + struct ANASTASIS_PaymentSecretP payment_secret; + + /** + * True if @e payment_secret is set. + */ + bool have_payment_secret; +}; + + +/** + * A decryption policy: solving all of its challenges recovers the + * secret. + */ +struct ANASTASIS_ReduxRecoveryPolicy +{ + /** + * UUIDs of the challenges in this policy; each refers to an entry of + * the recovery information's @e challenges array. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP *uuids; + + /** + * Length of the @e uuids array. + */ + unsigned int uuids_len; +}; + + +/** + * What we learned about the recovery document we are recovering from. + */ +struct ANASTASIS_ReduxRecoveryInfo +{ + /** + * All challenges referenced by any policy. + */ + struct ANASTASIS_ReduxChallengeInfo *challenges; + + /** + * Length of the @e challenges array. + */ + unsigned int challenges_len; + + /** + * Alternative ways to recover the secret. + */ + struct ANASTASIS_ReduxRecoveryPolicy *policies; + + /** + * Length of the @e policies array. + */ + unsigned int policies_len; + + /** + * Name the user gave the secret, NULL if unknown. + */ + char *secret_name; + + /** + * Provider we downloaded the recovery document from. + */ + struct ANASTASIS_ProviderUrl provider_url; + + /** + * Version of the recovery document. + */ + uint32_t version; +}; + + +/** + * State of a challenge, as reported back to the application. + */ +enum ANASTASIS_ReduxFeedbackStatus +{ + /** + * The TAN was written to a file (test setups only). + */ + ANASTASIS_RFS_CODE_IN_FILE = 0, + + /** + * The TAN was sent to the user out-of-band. + */ + ANASTASIS_RFS_SEND_TO_ADDRESS = 1, + + /** + * A Taler payment is required first. + */ + ANASTASIS_RFS_TALER_PAYMENT = 2, + + /** + * The provider failed. + */ + ANASTASIS_RFS_SERVER_FAILURE = 3, + + /** + * The provider does not know this truth. + */ + ANASTASIS_RFS_TRUTH_UNKNOWN = 4, + + /** + * A wire transfer is required first. + */ + ANASTASIS_RFS_IBAN_INSTRUCTIONS = 5, + + /** + * The challenge was solved. + */ + ANASTASIS_RFS_SOLVED = 6, + + /** + * The answer the user gave was wrong. + */ + ANASTASIS_RFS_INCORRECT_ANSWER = 7, + + /** + * Too many attempts. + */ + ANASTASIS_RFS_RATE_LIMIT_EXCEEDED = 8 +}; + + +/** + * Feedback on one challenge. Serializes as one entry of the + * `challenge_feedback` object, keyed by @e uuid. + */ +struct ANASTASIS_ReduxFeedback +{ + /** + * Challenge this feedback is about; the key this entry is stored + * under. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + + /** + * Which variant of @e details is valid. + */ + enum ANASTASIS_ReduxFeedbackStatus status; + + /** + * Human-readable, translated hint for the user. NULL for the + * variants that do not produce one. + */ + char *display_hint; + + /** + * Variant-specific details. + */ + union + { + + /** + * Details for #ANASTASIS_RFS_CODE_IN_FILE. + */ + struct + { + /** + * File the TAN was written to. + */ + char *filename; + } code_in_file; + + /** + * Details for #ANASTASIS_RFS_SEND_TO_ADDRESS. + */ + struct + { + /** + * Redacted address the TAN went to, NULL if the provider did not + * say (because it had already sent the TAN earlier). + */ + char *address_hint; + } send_to_address; + + /** + * Details for #ANASTASIS_RFS_TALER_PAYMENT. + */ + struct + { + /** + * URI the application must pay. + */ + char *taler_pay_uri; + + /** + * Provider that wants to be paid. + */ + struct ANASTASIS_ProviderUrl provider; + + /** + * Secret identifying the payment. + */ + struct ANASTASIS_PaymentSecretP payment_secret; + } taler_payment; + + /** + * Details for #ANASTASIS_RFS_SERVER_FAILURE and + * #ANASTASIS_RFS_TRUTH_UNKNOWN. + */ + struct + { + /** + * Error code reported. + */ + enum TALER_ErrorCode ec; + + /** + * HTTP status reported. + */ + uint32_t http_status; + } server_failure; + + /** + * Details for #ANASTASIS_RFS_IBAN_INSTRUCTIONS. + */ + struct + { + /** + * IBAN to wire the money to. + */ + char *target_iban; + + /** + * Name of the account holder. + */ + char *target_business_name; + + /** + * Subject to use for the wire transfer. + */ + char *wire_transfer_subject; + + /** + * Amount to wire. + */ + struct TALER_Amount amount; + } iban_instructions; + + /** + * Details for #ANASTASIS_RFS_INCORRECT_ANSWER. + */ + struct + { + /** + * Error code reported. + */ + enum TALER_ErrorCode ec; + } incorrect_answer; + + /** + * Details for #ANASTASIS_RFS_RATE_LIMIT_EXCEEDED. + */ + struct + { + /** + * Error code reported. + */ + enum TALER_ErrorCode ec; + + /** + * How many attempts are permitted per @e request_frequency. + */ + uint64_t request_limit; + + /** + * Length of the rate-limiting window. + */ + struct GNUNET_TIME_Relative request_frequency; + } rate_limit_exceeded; + + } details; +}; + + +/** + * The `recovery` variant of the reducer state. + */ +struct ANASTASIS_ReduxRecovery +{ + /** + * Which step of the recovery process we are at. + */ + enum ANASTASIS_RecoveryState state; + + /** + * The recovery operation itself. This replaces what used to be the + * `recovery_document` field: it is deserialized on parse (inert, no + * network activity until ANASTASIS_recovery_resume() is called) and + * serialized back on the way out. NULL before the recovery starts. + */ + struct ANASTASIS_Recovery *r; + + /** + * What we told the application about the recovery document, NULL + * before the recovery starts. + */ + struct ANASTASIS_ReduxRecoveryInfo *ri; + + /** + * Feedback on challenges the user tried, keyed by truth UUID. + */ + struct ANASTASIS_ReduxFeedback *challenge_feedback; + + /** + * Length of the @e challenge_feedback array. + */ + unsigned int challenge_feedback_len; + + /** + * Challenge the user is currently working on; only valid if + * @e have_selected_challenge. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP selected_challenge; + + /** + * True if @e selected_challenge is set. + */ + bool have_selected_challenge; + + /** + * True if the state has a @e challenge_feedback field at all. + */ + bool have_challenge_feedback; + + /** + * The recovered secret. Deliberately untyped, see + * ANASTASIS_ReduxBackup::core_secret. NULL until recovery succeeds. + */ + json_t *core_secret; +}; + + +/* ********************** Error state ******************************** */ + +/** + * The `error` variant of the reducer state. + */ +struct ANASTASIS_ReduxError +{ + /** + * The error that occurred. + */ + enum TALER_ErrorCode code; + + /** + * Hint derived from @e code, NULL if none. + */ + char *hint; + + /** + * Additional detail, NULL if none. + */ + char *detail; +}; + + +/* ********************** Top-level state **************************** */ + +/** + * Which variant of a #ANASTASIS_ReduxState is in use. + */ +enum ANASTASIS_ReduxType +{ + /** + * A backup is in progress; `reducer_type` is "backup". + */ + ANASTASIS_RT_BACKUP = 0, + + /** + * A recovery is in progress; `reducer_type` is "recovery". + */ + ANASTASIS_RT_RECOVERY = 1, + + /** + * Something went wrong; `reducer_type` is "error". + */ + ANASTASIS_RT_ERROR = 2 +}; + + +/** + * Fields shared by the backup and recovery variants. + */ +struct ANASTASIS_ReduxCommon +{ + /** + * Continents the user may pick from. + */ + struct ANASTASIS_ReduxContinent *continents; + + /** + * Length of the @e continents array. + */ + unsigned int continents_len; + + /** + * Countries on the selected continent. + */ + struct ANASTASIS_ReduxCountry *countries; + + /** + * Length of the @e countries array. + */ + unsigned int countries_len; + + /** + * Identity attributes we need from the user in the selected country. + */ + struct ANASTASIS_ReduxAttributeSpec *required_attributes; + + /** + * Length of the @e required_attributes array. + */ + unsigned int required_attributes_len; + + /** + * Providers we know about, keyed by URL. + */ + struct ANASTASIS_ReduxProvider *providers; + + /** + * Length of the @e providers array. + */ + unsigned int providers_len; + + /** + * The user's answers to @e required_attributes. Deliberately + * untyped: this object is dumped with sorted keys and PoW-hashed to + * derive the account key, so the reducer must not normalize it. NULL + * if the user has not answered yet. + */ + json_t *identity_attributes; + + /** + * Continent the user selected, NULL if none. + */ + char *selected_continent; + + /** + * Country the user selected, NULL if none. + */ + char *selected_country; + + /** + * True if the state has a @e continents field at all. An array can + * legitimately be present but empty, which is different from being + * absent, so presence is tracked separately from the length. + */ + bool have_continents; + + /** + * True if the state has a @e countries field at all. + */ + bool have_countries; + + /** + * True if the state has a @e required_attributes field at all. + */ + bool have_required_attributes; + + /** + * True if the state has an @e authentication_providers field at all. + */ + bool have_providers; + + /** + * Currencies in play, carried through for user interfaces. + */ + char **currencies; + + /** + * Length of the @e currencies array. + */ + unsigned int currencies_len; + + /** + * True if the state has a @e currencies field at all. + */ + bool have_currencies; +}; + + +/** + * A fully parsed reducer state. + */ +struct ANASTASIS_ReduxState +{ + /** + * Which variant of @e details is valid. + */ + enum ANASTASIS_ReduxType type; + + /** + * Fields common to the backup and recovery variants; unused when + * @e type is #ANASTASIS_RT_ERROR. + */ + struct ANASTASIS_ReduxCommon common; + + /** + * Variant-specific state. + */ + union + { + + /** + * Valid if @e type is #ANASTASIS_RT_BACKUP. + */ + struct ANASTASIS_ReduxBackup backup; + + /** + * Valid if @e type is #ANASTASIS_RT_RECOVERY. + */ + struct ANASTASIS_ReduxRecovery recovery; + + /** + * Valid if @e type is #ANASTASIS_RT_ERROR. + */ + struct ANASTASIS_ReduxError error; + + } details; +}; + + +/* ******************* The three generic routines ******************** */ + +/** + * Parse @a json into a typed reducer state. This is the only routine + * that reads the JSON encoding of a state. + * + * Parsing is strict: unknown fields, missing mandatory fields and + * values of the wrong type all fail. + * + * @param json the state to parse + * @param[out] ec set to the error that occurred, on failure + * @param[out] detail set to a static string naming the offending field, + * or NULL; never needs to be freed + * @return NULL on failure, otherwise the parsed state, to be freed with + * ANASTASIS_REDUX_state_free_() + */ +struct ANASTASIS_ReduxState * +ANASTASIS_REDUX_state_parse_ (const json_t *json, + enum TALER_ErrorCode *ec, + const char **detail); + + +/** + * Serialize @a rs back into its JSON encoding. This is the only + * routine that writes the JSON encoding of a state. + * + * @param rs the state to serialize + * @return the JSON encoding, caller must json_decref() it; + * NULL on failure + */ +json_t * +ANASTASIS_REDUX_state_serialize_ (const struct ANASTASIS_ReduxState *rs); + + +/** + * Release all resources held by @a rs, including any live + * `struct ANASTASIS_Recovery` and `struct ANASTASIS_Truth` handles. + * + * @param[in] rs the state to free, may be NULL + */ +void +ANASTASIS_REDUX_state_free_ (struct ANASTASIS_ReduxState *rs); + + +/** + * Serialize @a rs, hand the result to @a cb, and free @a rs. This is + * how every action returns its result: it guarantees that the JSON + * encoding is produced in exactly one place and that the state is not + * leaked on any path. + * + * @param[in] rs state to return and free + * @param cb callback to invoke + * @param cb_cls closure for @a cb + * @param ec error code to report alongside the state + */ +void +ANASTASIS_REDUX_return_ (struct ANASTASIS_ReduxState *rs, + ANASTASIS_ActionCallback cb, + void *cb_cls, + enum TALER_ErrorCode ec); + + +/* ************************ Small helpers **************************** */ + +/** + * Find the provider with the given @a url in @a common. + * + * @param common state to search + * @param url provider URL to look for + * @return NULL if not found + */ +struct ANASTASIS_ReduxProvider * +ANASTASIS_REDUX_provider_find_ (const struct ANASTASIS_ReduxCommon *common, + const char *url); + + +/** + * Drop all providers from @a common, freeing them. Used when the set + * of applicable providers changes wholesale, e.g. because the user + * picked a different country. + * + * @param[in,out] common state to clear + */ +void +ANASTASIS_REDUX_providers_clear_ (struct ANASTASIS_ReduxCommon *common); + + +/** + * Find, or create, the provider with the given @a url in @a common. + * + * @param[in,out] common state to search and possibly grow + * @param url provider URL to look for + * @return the existing or newly added (and #ANASTASIS_RPS_NOT_CONTACTED) + * provider entry + */ +struct ANASTASIS_ReduxProvider * +ANASTASIS_REDUX_provider_get_ (struct ANASTASIS_ReduxCommon *common, + const char *url); + + +/** + * Find the feedback for challenge @a uuid in @a rr. + * + * @param rr recovery state to search + * @param uuid challenge to look for + * @return NULL if not found + */ +struct ANASTASIS_ReduxFeedback * +ANASTASIS_REDUX_feedback_find_ (const struct ANASTASIS_ReduxRecovery *rr, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid); + + +/** + * Find, or create, the feedback entry for challenge @a uuid in @a rr. + * An existing entry is reset (its variant-specific fields freed) so + * that the caller can fill in a fresh variant. + * + * @param[in,out] rr recovery state to search and possibly grow + * @param uuid challenge to look for + * @return the entry to fill in + */ +struct ANASTASIS_ReduxFeedback * +ANASTASIS_REDUX_feedback_get_ (struct ANASTASIS_ReduxRecovery *rr, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid); + + +/** + * Free the variant-specific contents of @a fb (including the display + * hint), but not @a fb itself, and zero them. Used both when freeing + * a state and when overwriting a feedback entry with a new variant. + * + * @param[in,out] fb feedback to reset + */ +void +ANASTASIS_REDUX_feedback_clear_ (struct ANASTASIS_ReduxFeedback *fb); + + +/** + * Find the challenge with the given @a uuid in the recovery + * information of @a rr. + * + * @param rr recovery state to search + * @param uuid challenge to look for + * @return NULL if not found + */ +struct ANASTASIS_ReduxChallengeInfo * +ANASTASIS_REDUX_challenge_find_ (const struct ANASTASIS_ReduxRecovery *rr, + const struct + ANASTASIS_CRYPTO_TruthUUIDP *uuid); + + +/** + * Free the contents of @a pu and set it to NULL. + * + * @param[in,out] pu provider URL to clear + */ +void +ANASTASIS_REDUX_provider_url_clear_ (struct ANASTASIS_ProviderUrl *pu); + + +/** + * Set @a pu to a copy of @a url, freeing any previous value. + * + * @param[in,out] pu provider URL to set + * @param url the URL to copy + */ +void +ANASTASIS_REDUX_provider_url_set_ (struct ANASTASIS_ProviderUrl *pu, + const char *url); + + +#endif + +/* end of anastasis_api_redux_state.h */ diff --git a/src/reducer/test_redux_codec.c b/src/reducer/test_redux_codec.c @@ -0,0 +1,365 @@ +/* + This file is part of Anastasis + Copyright (C) 2022 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file reducer/test_redux_codec.c + * @brief tests for the typed reducer state codec + * @author Christian Grothoff + * + * Checks three properties of ANASTASIS_REDUX_state_parse_() and + * ANASTASIS_REDUX_state_serialize_(): + * + * - every state the CLI test suite feeds the reducer parses; + * - the codec is idempotent (serialize . parse . serialize == serialize . + * parse), so no field is lost or invented on the way through; + * - unknown fields are rejected rather than silently dropped. + * + * The codec is also exercised end-to-end against live states by the `cli` + * suite, which drives the real reducer; this test is the fast, offline part + * that needs neither a database nor a provider. + */ +#include "platform.h" +#include "anastasis_redux.h" +#include "anastasis_api_redux.h" +#include "anastasis_api_redux_state.h" +#include <taler/taler_json_lib.h> + + +/** + * Return value of the test, 0 on success. + */ +static int global_ret; + + +/** + * Check that @a json parses and that the codec is idempotent on it. + * + * @param name name of the input, for error messages + * @param json state to check + */ +static void +check_idempotent (const char *name, + const json_t *json) +{ + struct ANASTASIS_ReduxState *rs; + enum TALER_ErrorCode ec; + const char *detail; + json_t *once; + json_t *twice; + + rs = ANASTASIS_REDUX_state_parse_ (json, + &ec, + &detail); + if (NULL == rs) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "%s: failed to parse at `%s'\n", + name, + detail); + global_ret = 1; + return; + } + once = ANASTASIS_REDUX_state_serialize_ (rs); + GNUNET_assert (NULL != once); + ANASTASIS_REDUX_state_free_ (rs); + + rs = ANASTASIS_REDUX_state_parse_ (once, + &ec, + &detail); + if (NULL == rs) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "%s: own output failed to re-parse at `%s'\n", + name, + detail); + json_decref (once); + global_ret = 1; + return; + } + twice = ANASTASIS_REDUX_state_serialize_ (rs); + GNUNET_assert (NULL != twice); + ANASTASIS_REDUX_state_free_ (rs); + + if (1 != json_equal (once, + twice)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "%s: codec is not idempotent\n", + name); + json_dumpf (once, + stderr, + JSON_INDENT (2) | JSON_SORT_KEYS); + json_dumpf (twice, + stderr, + JSON_INDENT (2) | JSON_SORT_KEYS); + global_ret = 1; + } + json_decref (once); + json_decref (twice); +} + + +/** + * Check that @a json is rejected by the parser. + * + * @param name name of the input, for error messages + * @param json state that must not parse + */ +static void +check_rejected (const char *name, + const json_t *json) +{ + struct ANASTASIS_ReduxState *rs; + enum TALER_ErrorCode ec; + const char *detail; + + rs = ANASTASIS_REDUX_state_parse_ (json, + &ec, + &detail); + if (NULL != rs) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "%s: expected rejection, but the state parsed\n", + name); + ANASTASIS_REDUX_state_free_ (rs); + global_ret = 1; + return; + } + if (TALER_EC_ANASTASIS_REDUCER_STATE_INVALID != ec) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "%s: rejected with unexpected error code %d\n", + name, + (int) ec); + global_ret = 1; + } +} + + +/** + * Run the codec over every state fixture the CLI tests use. + * + * @param srcdir directory holding the `resources/` fixtures + */ +static void +test_fixtures (const char *srcdir) +{ + static const char *const files[] = { + "00-backup.json", + "01-backup.json", + "02-backup.json", + "03-backup.json", + "04-backup.json", + "05-backup.json", + "06-backup.json", + "00-recovery.json", + "01-recovery.json", + "02-recovery.json", + NULL + }; + + for (unsigned int i = 0; NULL != files[i]; i++) + { + json_error_t err; + json_t *json; + char *fn; + + GNUNET_asprintf (&fn, + "%s/resources/%s", + srcdir, + files[i]); + json = json_load_file (fn, + JSON_REJECT_DUPLICATES, + &err); + if (NULL == json) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load `%s': %s\n", + fn, + err.text); + GNUNET_free (fn); + global_ret = 1; + continue; + } + check_idempotent (files[i], + json); + json_decref (json); + GNUNET_free (fn); + } +} + + +/** + * Check that every challenge feedback variant survives the codec. + * These are the states with the most structure (a tagged union), and + * several of them only occur against a live provider, so they are + * covered here rather than by the CLI tests. + */ +static void +test_challenge_feedback (void) +{ + static const char *const variants[] = { + "{\"state\":\"code-in-file\",\"filename\":\"/tmp/tan\"," + "\"display_hint\":\"look here\"}", + "{\"state\":\"send-to-address\",\"address_hint\":\"a@b.c\"," + "\"display_hint\":\"sent\"}", + "{\"state\":\"send-to-address\",\"display_hint\":\"already sent\"}", + "{\"state\":\"taler-payment\",\"taler_pay_uri\":\"taler://pay/x\"," + "\"provider\":\"http://localhost:8086/\",\"display_hint\":\"pay\"," + "\"payment_secret\":\"3F1QF6ZY2H0V4CQNVQ8YV2P8V0Y0EWWG14JBQZ3M4P2C1Y8XM53G\"}", + "{\"state\":\"server-failure\",\"http_status\":500,\"error_code\":1}", + "{\"state\":\"truth-unknown\",\"http_status\":404,\"error_code\":8108}", + "{\"state\":\"iban-instructions\",\"target_iban\":\"DE12\"," + "\"target_business_name\":\"Bank\",\"wire_transfer_subject\":\"subj\"," + "\"challenge_amount\":\"TESTKUDOS:1\",\"display_hint\":\"wire it\"}", + "{\"state\":\"solved\"}", + "{\"state\":\"incorrect-answer\",\"error_code\":8110}", + "{\"state\":\"rate-limit-exceeded\",\"error_code\":8111," + "\"request_limit\":3,\"request_frequency\":{\"d_us\":1000000}," + "\"display_hint\":\"slow down\"}", + NULL + }; + static const char *const uuid + = "3AHRZ6DMV29GQY5CKT18FPX4BJS07ENW3AHRZ6DMV29GQY5CKT18"; + + for (unsigned int i = 0; NULL != variants[i]; i++) + { + json_error_t err; + json_t *fb; + json_t *state; + + fb = json_loads (variants[i], + JSON_REJECT_DUPLICATES, + &err); + GNUNET_assert (NULL != fb); + state = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("reducer_type", + "recovery"), + GNUNET_JSON_pack_string ("recovery_state", + "CHALLENGE_SOLVING")); + { + json_t *cf = json_object (); + + GNUNET_assert (NULL != cf); + GNUNET_assert (0 == + json_object_set_new (cf, + uuid, + fb)); + GNUNET_assert (0 == + json_object_set_new (state, + "challenge_feedback", + cf)); + } + check_idempotent (variants[i], + state); + json_decref (state); + } +} + + +/** + * Check that fields the reducer does not know about are rejected + * instead of being silently carried along or dropped. + */ +static void +test_strictness (void) +{ + static const char *const bad[] = { + /* unknown field at the top level (this is the shape the TypeScript + reducer produces; see dold.md) */ + "{\"reducer_type\":\"recovery\",\"recovery_state\":\"SECRET_SELECTING\"," + "\"verbatim_recovery_document\":{}}", + /* unknown field inside a nested object */ + "{\"reducer_type\":\"backup\",\"backup_state\":\"CONTINENT_SELECTING\"," + "\"continents\":[{\"name\":\"Europe\",\"bogus\":1}]}", + /* unknown provider status */ + "{\"reducer_type\":\"backup\",\"backup_state\":\"CONTINENT_SELECTING\"," + "\"authentication_providers\":{\"http://x/\":{\"status\":\"weird\"}}}", + /* unknown challenge feedback variant */ + "{\"reducer_type\":\"recovery\",\"recovery_state\":\"CHALLENGE_SOLVING\"," + "\"challenge_feedback\":{\"3AHRZ6DMV29GQY5CKT18FPX4BJS07ENW3AHRZ6DMV29GQ" + "Y5CKT18\":{\"state\":\"invented\"}}}", + /* bogus state name */ + "{\"reducer_type\":\"backup\",\"backup_state\":\"NOT_A_STATE\"}", + /* neither backup nor recovery nor error */ + "{\"reducer_type\":\"sideways\"}", + NULL + }; + + for (unsigned int i = 0; NULL != bad[i]; i++) + { + json_error_t err; + json_t *json; + + json = json_loads (bad[i], + JSON_REJECT_DUPLICATES, + &err); + GNUNET_assert (NULL != json); + check_rejected (bad[i], + json); + json_decref (json); + } +} + + +/** + * Check that an `error` state survives the codec. + */ +static void +test_error_state (void) +{ + json_t *json; + + json = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("reducer_type", + "error"), + GNUNET_JSON_pack_uint64 ("code", + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID), + GNUNET_JSON_pack_string ("hint", + "a hint"), + GNUNET_JSON_pack_string ("detail", + "a detail")); + check_idempotent ("error state", + json); + json_decref (json); +} + + +int +main (int argc, + char *const *argv) +{ + const char *srcdir; + + (void) argc; + GNUNET_log_setup ("test-redux-codec", + "WARNING", + NULL); + srcdir = getenv ("ANASTASIS_CLI_SRCDIR"); + if (NULL == srcdir) + { + fprintf (stderr, + "ANASTASIS_CLI_SRCDIR not set, skipping\n"); + return 77; + } + test_fixtures (srcdir); + test_challenge_feedback (); + test_strictness (); + test_error_state (); + return global_ret; +} + + +/* end of test_redux_codec.c */