commit 4fefdf6262bdffa6a4cfedd5fd25e24821787def
parent b54b60be71dbac974e247b9688186dbc3d896c74
Author: Christian Grothoff <christian@grothoff.org>
Date: Wed, 29 Jul 2026 10:44:31 +0200
refactor library to separate mere parsing from starting network activities
Diffstat:
4 files changed, 225 insertions(+), 41 deletions(-)
diff --git a/src/include/anastasis.h b/src/include/anastasis.h
@@ -81,6 +81,18 @@ struct ANASTASIS_ChallengeDetails
*/
bool async;
+ /**
+ * true if @e answer_pin is set.
+ */
+ bool have_answer_pin;
+
+ /**
+ * Answer code the user must eventually confirm for an asynchronous
+ * challenge; only valid if @e have_answer_pin. Persisted across
+ * serialization so that a suspended recovery can resume polling.
+ */
+ uint64_t answer_pin;
+
};
@@ -95,6 +107,21 @@ ANASTASIS_challenge_get_details (struct ANASTASIS_Challenge *challenge);
/**
+ * Mark @a challenge as awaiting asynchronous resolution by the user,
+ * remembering the @a answer_pin the user must eventually confirm.
+ *
+ * Both values are part of the serialized form of the recovery
+ * operation, so they survive suspending and resuming a recovery.
+ *
+ * @param[in,out] challenge the challenge to mark
+ * @param answer_pin answer code to remember
+ */
+void
+ANASTASIS_challenge_set_async (struct ANASTASIS_Challenge *challenge,
+ uint64_t answer_pin);
+
+
+/**
* Possible outcomes of trying to start a challenge operation.
*/
enum ANASTASIS_ChallengeStartStatus
@@ -720,21 +747,67 @@ ANASTASIS_recovery_serialize (const struct ANASTASIS_Recovery *r);
/**
* Deserialize recovery operation.
*
+ * The returned handle is inert: no callbacks are installed and no
+ * network activity is started. Call #ANASTASIS_recovery_resume() to
+ * continue the recovery, or simply #ANASTASIS_recovery_serialize() it
+ * again and #ANASTASIS_recovery_abort() it if all you needed was to
+ * inspect it.
+ *
* @param ctx context for making HTTP requests
* @param input result from #ANASTASIS_recovery_serialize()
+ * @return recovery operation handle, NULL if @a input is malformed
+ */
+struct ANASTASIS_Recovery *
+ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx,
+ const json_t *input);
+
+
+/**
+ * Resume a recovery operation obtained from
+ * #ANASTASIS_recovery_deserialize(): install the callbacks and start
+ * the policy lookup (or, if the policies are already known, arrange
+ * for @a pc to be called from the scheduler).
+ *
+ * May be called at most once per handle.
+ *
+ * @param[in,out] r recovery operation to resume
* @param pc opens the policy call back which holds the downloaded version and the policies
* @param pc_cls closure for callback
* @param csc core secret callback is opened, with this the core secert is passed to the client after the authentication
* @param csc_cls handle for the callback
- * @return recovery operation handle
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR if the lookup could
+ * not be started
*/
-struct ANASTASIS_Recovery *
-ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx,
- const json_t *input,
- ANASTASIS_PolicyCallback pc,
- ANASTASIS_POLICY_RESULT_CLOSURE *pc_cls,
- ANASTASIS_CoreSecretCallback csc,
- ANASTASIS_CORE_SECRET_RESULT_CLOSURE *csc_cls);
+enum GNUNET_GenericReturnValue
+ANASTASIS_recovery_resume (struct ANASTASIS_Recovery *r,
+ ANASTASIS_PolicyCallback pc,
+ ANASTASIS_POLICY_RESULT_CLOSURE *pc_cls,
+ ANASTASIS_CoreSecretCallback csc,
+ ANASTASIS_CORE_SECRET_RESULT_CLOSURE *csc_cls);
+
+
+/**
+ * Obtain the recovery information (policies and challenges) known for
+ * @a r. Valid until @a r is aborted.
+ *
+ * @param r recovery operation to inspect
+ * @return recovery information, never NULL
+ */
+const struct ANASTASIS_RecoveryInformation *
+ANASTASIS_recovery_get_info (const struct ANASTASIS_Recovery *r);
+
+
+/**
+ * Find the challenge with the given @a uuid in @a r.
+ *
+ * @param r recovery operation to search
+ * @param uuid UUID of the challenge to find
+ * @return NULL if @a r has no such challenge
+ */
+struct ANASTASIS_Challenge *
+ANASTASIS_recovery_get_challenge (
+ const struct ANASTASIS_Recovery *r,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid);
/**
diff --git a/src/lib/anastasis_recovery.c b/src/lib/anastasis_recovery.c
@@ -466,6 +466,41 @@ ANASTASIS_challenge_get_details (struct ANASTASIS_Challenge *challenge)
}
+void
+ANASTASIS_challenge_set_async (struct ANASTASIS_Challenge *challenge,
+ uint64_t answer_pin)
+{
+ challenge->ci.async = true;
+ challenge->ci.have_answer_pin = true;
+ challenge->ci.answer_pin = answer_pin;
+}
+
+
+const struct ANASTASIS_RecoveryInformation *
+ANASTASIS_recovery_get_info (const struct ANASTASIS_Recovery *r)
+{
+ return &r->ri;
+}
+
+
+struct ANASTASIS_Challenge *
+ANASTASIS_recovery_get_challenge (
+ const struct ANASTASIS_Recovery *r,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid)
+{
+ for (unsigned int i = 0; i < r->ri.cs_len; i++)
+ {
+ struct ANASTASIS_Challenge *c = r->ri.cs[i];
+
+ if (0 ==
+ GNUNET_memcmp (uuid,
+ &c->ci.uuid))
+ return c;
+ }
+ return NULL;
+}
+
+
enum GNUNET_GenericReturnValue
ANASTASIS_challenge_start (struct ANASTASIS_Challenge *c,
const struct ANASTASIS_PaymentSecretP *psp,
@@ -1107,7 +1142,11 @@ ANASTASIS_recovery_serialize (const struct ANASTASIS_Recovery *r)
GNUNET_JSON_pack_bool ("solved",
c->ci.solved),
GNUNET_JSON_pack_bool ("async",
- c->ci.async));
+ c->ci.async),
+ GNUNET_JSON_pack_conditional (
+ c->ci.have_answer_pin,
+ GNUNET_JSON_pack_uint64 ("answer-pin",
+ c->ci.answer_pin)));
GNUNET_assert (0 ==
json_array_append_new (cs_arr,
cs));
@@ -1177,6 +1216,7 @@ parse_cs_array (struct ANASTASIS_Recovery *r,
const char *url;
const char *escrow_type;
bool no_key_share;
+ bool no_answer_pin;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_fixed_auto ("uuid",
&c->ci.uuid),
@@ -1197,6 +1237,10 @@ parse_cs_array (struct ANASTASIS_Recovery *r,
&c->ci.async),
NULL),
GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint64 ("answer-pin",
+ &c->ci.answer_pin),
+ &no_answer_pin),
+ GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_fixed_auto ("key_share",
&c->key_share),
&no_key_share),
@@ -1219,6 +1263,7 @@ parse_cs_array (struct ANASTASIS_Recovery *r,
c->instructions = GNUNET_strdup (instructions);
c->ci.instructions = c->instructions;
c->ci.provider_url = c->url;
+ c->ci.have_answer_pin = ! no_answer_pin;
if (! no_key_share)
{
c->ci.solved = true;
@@ -1369,19 +1414,11 @@ run_async_pc (void *cls)
struct ANASTASIS_Recovery *
ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx,
- const json_t *input,
- ANASTASIS_PolicyCallback pc,
- void *pc_cls,
- ANASTASIS_CoreSecretCallback csc,
- void *csc_cls)
+ const json_t *input)
{
struct ANASTASIS_Recovery *r;
r = GNUNET_new (struct ANASTASIS_Recovery);
- r->csc = csc;
- r->csc_cls = csc_cls;
- r->pc = pc;
- r->pc_cls = pc_cls;
r->ctx = ctx;
{
const char *err_json_name;
@@ -1457,6 +1494,24 @@ ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx,
}
GNUNET_JSON_parse_free (spec);
}
+ return r;
+}
+
+
+enum GNUNET_GenericReturnValue
+ANASTASIS_recovery_resume (struct ANASTASIS_Recovery *r,
+ ANASTASIS_PolicyCallback pc,
+ void *pc_cls,
+ ANASTASIS_CoreSecretCallback csc,
+ void *csc_cls)
+{
+ GNUNET_assert (NULL == r->pc);
+ GNUNET_assert (NULL == r->plo);
+ GNUNET_assert (NULL == r->do_async);
+ r->csc = csc;
+ r->csc_cls = csc_cls;
+ r->pc = pc;
+ r->pc_cls = pc_cls;
if (0 == r->ri.dps_len)
{
struct ANASTASIS_CRYPTO_AccountPublicKeyP pub_key;
@@ -1483,8 +1538,7 @@ ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx,
if (NULL == r->plo)
{
GNUNET_break (0);
- ANASTASIS_recovery_abort (r);
- return NULL;
+ return GNUNET_SYSERR;
}
}
else
@@ -1492,7 +1546,7 @@ ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx,
r->do_async = GNUNET_SCHEDULER_add_now (&run_async_pc,
r);
}
- return r;
+ return GNUNET_OK;
}
diff --git a/src/reducer/anastasis_api_recovery_redux.c b/src/reducer/anastasis_api_recovery_redux.c
@@ -1545,11 +1545,18 @@ solve_challenge (json_t *state,
sctx->state = json_incref (state);
sctx->args = json_incref ((json_t*) arguments);
sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_,
- rd,
- &solve_challenge_cb,
- sctx,
- &core_secret_cb,
- sctx);
+ rd);
+ if ( (NULL != sctx->r) &&
+ (GNUNET_OK !=
+ ANASTASIS_recovery_resume (sctx->r,
+ &solve_challenge_cb,
+ sctx,
+ &core_secret_cb,
+ sctx)) )
+ {
+ ANASTASIS_recovery_abort (sctx->r);
+ sctx->r = NULL;
+ }
if (NULL == sctx->r)
{
json_decref (sctx->state);
@@ -1606,11 +1613,18 @@ poll_challenges (json_t *state,
sctx->state = json_incref (state);
sctx->args = json_incref ((json_t*) arguments);
sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_,
- rd,
- &solve_challenge_cb,
- sctx,
- &core_secret_cb,
- sctx);
+ rd);
+ if ( (NULL != sctx->r) &&
+ (GNUNET_OK !=
+ ANASTASIS_recovery_resume (sctx->r,
+ &solve_challenge_cb,
+ sctx,
+ &core_secret_cb,
+ sctx)) )
+ {
+ ANASTASIS_recovery_abort (sctx->r);
+ sctx->r = NULL;
+ }
if (NULL == sctx->r)
{
json_decref (sctx->state);
@@ -1711,11 +1725,18 @@ pay_challenge (json_t *state,
sctx->state = json_incref (state);
sctx->args = json_incref ((json_t*) arguments);
sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_,
- rd,
- &pay_challenge_cb,
- sctx,
- &core_secret_cb,
- sctx);
+ rd);
+ if ( (NULL != sctx->r) &&
+ (GNUNET_OK !=
+ ANASTASIS_recovery_resume (sctx->r,
+ &pay_challenge_cb,
+ sctx,
+ &core_secret_cb,
+ sctx)) )
+ {
+ ANASTASIS_recovery_abort (sctx->r);
+ sctx->r = NULL;
+ }
if (NULL == sctx->r)
{
json_decref (sctx->state);
@@ -1995,11 +2016,18 @@ select_challenge (json_t *state,
sctx->state = json_incref (state);
sctx->args = json_incref ((json_t*) arguments);
sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_,
- rd,
- &select_challenge_cb,
- sctx,
- &core_secret_cb,
- sctx);
+ rd);
+ if ( (NULL != sctx->r) &&
+ (GNUNET_OK !=
+ ANASTASIS_recovery_resume (sctx->r,
+ &select_challenge_cb,
+ sctx,
+ &core_secret_cb,
+ sctx)) )
+ {
+ ANASTASIS_recovery_abort (sctx->r);
+ sctx->r = NULL;
+ }
if (NULL == sctx->r)
{
json_decref (sctx->state);
diff --git a/src/reducer/meson.build b/src/reducer/meson.build
@@ -3,6 +3,10 @@ libanastasisredux_SOURCES = [
'anastasis_api_discovery.c',
'anastasis_api_providers.c',
'anastasis_api_redux.c',
+ 'anastasis_api_redux_state.c',
+ 'anastasis_api_redux_parse.c',
+ 'anastasis_api_redux_serialize.c',
+ 'anastasis_api_redux_free.c',
'anastasis_api_recovery_redux.c',
'anastasis_api_backup_redux.c',
'validation_CH_AHV.c',
@@ -46,6 +50,31 @@ libanastasisredux = library(
install_dir: get_option('libdir'),
)
+test_redux_codec = executable(
+ 'test_redux_codec',
+ ['test_redux_codec.c'],
+ dependencies: [
+ gnunetutil_dep,
+ gnunetjson_dep,
+ gnunetcurl_dep,
+ talerutil_dep,
+ talerjson_dep,
+ json_dep,
+ libanastasisutil_dep,
+ libanastasis_dep,
+ declare_dependency(link_with: libanastasisredux),
+ ],
+ include_directories: [incdir, configuration_inc],
+ build_by_default: false,
+ install: false,
+)
+test(
+ 'test_redux_codec',
+ test_redux_codec,
+ env: {'ANASTASIS_CLI_SRCDIR': meson.project_source_root() / 'src' / 'cli'},
+ suite: ['reducer'],
+)
+
libanastasisredux_dep = declare_dependency(link_with: libanastasisredux)
pkg.generate(
libanastasisredux,