summaryrefslogtreecommitdiff
path: root/src/stasis
diff options
context:
space:
mode:
Diffstat (limited to 'src/stasis')
-rw-r--r--src/stasis/Datenbank-Schema.xml1
-rw-r--r--src/stasis/Makefile.am95
-rw-r--r--src/stasis/anastasis-dbinit.c112
-rw-r--r--src/stasis/anastasis_db_plugin.c146
-rw-r--r--src/stasis/anastasis_db_postgres.conf7
-rw-r--r--src/stasis/drop0001.sql37
-rw-r--r--src/stasis/plugin_anastasis_postgres.c2301
-rw-r--r--src/stasis/stasis-0000.sql293
-rw-r--r--src/stasis/stasis-0001.sql194
-rw-r--r--src/stasis/stasis-postgres.conf6
-rw-r--r--src/stasis/test_anastasis_db.c344
-rw-r--r--src/stasis/test_anastasis_db_postgres.conf10
12 files changed, 3546 insertions, 0 deletions
diff --git a/src/stasis/Datenbank-Schema.xml b/src/stasis/Datenbank-Schema.xml
new file mode 100644
index 0000000..0591d22
--- /dev/null
+++ b/src/stasis/Datenbank-Schema.xml
@@ -0,0 +1 @@
+<mxfile host="Electron" modified="2020-06-11T14:26:00.822Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.2.1 Chrome/83.0.4103.100 Electron/9.0.3 Safari/537.36" etag="akhPIe-celB06UuOFgWh" version="13.2.1" type="device"><diagram id="UF0pCA7TQTvobU7b0bed" name="Seite-1">7V1bc9o4GP01zOw+bMdXcB6B0sts0mYSdrd9YhRbwZ7aFiOLBPrrV7IlX5BpXGyDmVGbmaCrLX1HR0f6JDIy59HuIwYb/w55MBwZmrcbme9HhqHrmkF/sZh9FuPYWhaxxoHHMxURj8FPyCNFtm3gwaSSkSAUkmBTjXRRHEOXVOIAxui1mu0ZhdWnbsAaShGPLgjl2P8Cj/iiXeObIuETDNY+f7RjTLKECIjMvCWJDzz0WooyFyNzjhEi2adoN4ch6zzRL1m5D0dS8xfDMCZNCnBDvIBwy9v2TwIxfzeyFw1OXoMoBDENzZ5RTB55ikbDrh+E3i3Yoy17YEKA+0OEZj7CwU+aH4Q0SacRNBkTbk9jzGoLwnCOQoTT55hQY/8rJR9ZjfxZGCa07L1onX4QdQd2lYy3ICHiLVEYgk0SPKXvzQpGAK+DeIYIQRHPJFr5ofpSz+k/mg7CYB3TOJc+i3YS74usNbpFw3Lvc4O8QEzgrhTFrfERoggSvKdZeKp1w5HBh4ah8SpeC6Dl48CvYIzjm2N7nVddmJ9+4AioR4MpoWFL0bBiydPZ9+ViKgPDBxv2kbaZBCB8oKMNxGuWOiNow/s1hM/CDpi/MPv8JPpeL/VtlnfGOiygA27Ko6PA89Jay4CJUYrIZAPcIF7fZk8xrSLqgT+NRSFa5XOYjjWfVgZj9jaIAAKecixvUBCTtP/sGf2hvTzX3tkjm7Z0TsN6EaY/LDsmcxQnBIMgNTikgHuFDHQzD6PNkkIMitaWx419gB2jFjtGY+wIrIybQcXU2kPFkqBy/3drdDyVR2ONpZuCpIqIzgDxls1ZMzmc37auecS6+azFX6MyMVSsXjKz2ZOVdZkR4G4TYEACFK88QGjkdPn5bvG4nN7dX4wd0squnBq6pQK7IRUIymgFEpkLLs0EGSCugQb03nmgExPbkomXeEufqoTipYSioVWFol43+9s1eNCdLgAxlgBBGCAyrfjv9GH+afrwx9j6UynGMyrGfJi2kox1oOlETEyUZmw5WYyP2HdQorFmbQBjF+83BHqrH3C/oibGsFhS0kaZupY3uGuu0JR+PIEY6gRkHTF0oS4MWV1cmhauR0Dmo23YAtKRTAyoXKCtoL2eLiZplT4qiQdFBsMhA7G3eA4y0G8UGZwuEJwjph0UGQjnh7R4iAJa156ZVrHA8FhA15wzagJdwoiigcaaQDti22HRgOx8LNYJB+rAo13XrxNKccJJnGBaZ+QE2RuhOKExJxzzIQyKE0xZGhQOp/59TYoETiKBsXY+EjCVMGjhdL4KYWDKwoCakHXd3kPuNmLNUY6nSzmeJpOq48m0a0Z/na9Zd6wOsPGrM0oVv9O8tLNMW5dkE8jnL8t+pg7lkqpnnGOyo/UpJrsLNKljTG1nlN7PL3Rh57HsesR8RjlYWpacUcov1bvW/H12cBpONl3okLHstLg0N1yP1MyH3KCl5kSeACrEsPJB4it2uBZ20LUznnucKLf16fQwuQq39UQ+5nawlFCMMHBGEEXOwgiyzFSM0JgRej/c1omJaw6yuC7axmSVBGulFK6GF+waZ3ZvvKDOtLTghas402LJbol7sGdb1YlsarVXPbrMXrV103DQd3JFUnZjbDJIZLvVaiv6vFvRVnP4XOJCrezYUFvRvzVR5ONt0LcjbHlJeZZL9komnsIA57xHa8urxw8XZ4Dr0Yn2VawfLXnDeYMSskqXkEwG9ScLFAOcwgB1dx/6YgBL7Si3mP6vYkfZkqd/sShIoIsZlJUKGBoH1N586I0E1CZyCxK4ChEwlnmetjxOgJtediBBBNVZ54FSQd2Fh94OoMiThaKCxgdQjp1jHxQViMOzXZpYDffuhnvd1Ya64T7pYmtIXW1osfzv/WpDFyYeyz4BycTQW0Oxs82uQJL9AwzTC1CLIkXqEZ9Ewi8EY2/KvuWWBhcP1MxLdAfiPUvYBeSbyEQ/f2dmeWfz0Hvh70kDexGIaRO/lQOlUixYFEtDopxsIW6NBG2xy5vK97yJGMajYhuH9cIvR2jJWrpWY648Eqe99wIrb1FnRP6MewbSUemLtrISYkfQOoBB1h5eqECCVI9+UJF1WFHWC1JFKaTydjfzNzcgkh5RJhDzl/ZO0+wqbEzTfAM4aegeYvZdAGxDLIfk7yL3KALLcBPn1wcDt8nB97pJtNMUb4fAdfTe8ObIIgZE6ZmYF+asntKkz1+WjhI2vd3ZbKxrjjjGjXMenHWUzjld5zi965xOTCzrHM4Izxi4OSUoRhguI5zz4KyjvtmhBSMYR2w9LEaocXyC9NADjZx9/Xq7mH5RdDBcOhCnE85CB8oP2oIOhugHpcHij9pkK4ziTwOZi/8B</diagram></mxfile> \ No newline at end of file
diff --git a/src/stasis/Makefile.am b/src/stasis/Makefile.am
new file mode 100644
index 0000000..a1d6584
--- /dev/null
+++ b/src/stasis/Makefile.am
@@ -0,0 +1,95 @@
+# This Makefile.am is in the public domain
+AM_CPPFLAGS = -I$(top_srcdir)/src/include
+
+plugindir = $(libdir)/anastasis
+
+if HAVE_POSTGRESQL
+plugin_LTLIBRARIES = \
+ libanastasis_plugin_db_postgres.la
+endif
+
+if USE_COVERAGE
+ AM_CFLAGS = --coverage -O0
+ XLIB = -lgcov
+endif
+
+sqldir = $(prefix)/share/anastasis/sql/
+
+sql_DATA = \
+ stasis-0000.sql \
+ stasis-0001.sql \
+ drop0001.sql
+
+pkgcfgdir = $(prefix)/share/anastasis/config.d/
+
+pkgcfg_DATA = \
+ stasis-postgres.conf
+
+bin_PROGRAMS = \
+ anastasis-dbinit
+
+anastasis_dbinit_SOURCES = \
+ anastasis-dbinit.c
+
+anastasis_dbinit_LDADD = \
+ $(LIBGCRYPT_LIBS) \
+ libanastasisdb.la \
+ $(top_builddir)/src/util/libanastasisutil.la \
+ -lgnunetutil \
+ -ltalerutil \
+ -ltalerpq \
+ $(XLIB)
+
+
+lib_LTLIBRARIES = \
+ libanastasisdb.la
+
+libanastasisdb_la_SOURCES = \
+ anastasis_db_plugin.c
+libanastasisdb_la_LIBADD = \
+ -lgnunetpq \
+ -lpq \
+ -lgnunetutil \
+ -lltdl \
+ $(XLIB)
+libanastasisdb_la_LDFLAGS = \
+ $(POSTGRESQL_LDFLAGS) \
+ -version-info 2:0:0 \
+ -no-undefined
+
+libanastasis_plugin_db_postgres_la_SOURCES = \
+ plugin_anastasis_postgres.c
+libanastasis_plugin_db_postgres_la_LIBADD = \
+ $(LTLIBINTL)
+libanastasis_plugin_db_postgres_la_LDFLAGS = \
+ $(top_builddir)/src/util/libanastasisutil.la \
+ $(ANASTASIS_PLUGIN_LDFLAGS) \
+ -lgnunetpq \
+ -lpq \
+ -ltalerpq \
+ -ltalerutil \
+ -lgnunetutil \
+ $(XLIB)
+
+check_PROGRAMS = \
+ $(TESTS)
+
+test_anastasis_db_postgres_SOURCES = \
+ test_anastasis_db.c
+test_anastasis_db_postgres_LDFLAGS = \
+ $(top_builddir)/src/util/libanastasisutil.la \
+ libanastasisdb.la \
+ -lgnunetutil \
+ -lgnunetpq \
+ -ltalerutil \
+ -ltalerpq \
+ -luuid \
+ $(XLIB)
+
+AM_TESTS_ENVIRONMENT=export ANASTASIS_PREFIX=$${ANASTASIS_PREFIX:-@libdir@};export PATH=$${ANASTASIS_PREFIX:-@prefix@}/bin:$$PATH;unset XDG_DATA_HOME;unset XDG_CONFIG_HOME;
+TESTS = \
+ test_anastasis_db-postgres
+
+EXTRA_DIST = \
+ test_anastasis_db_postgres.conf \
+ $(sql_DATA)
diff --git a/src/stasis/anastasis-dbinit.c b/src/stasis/anastasis-dbinit.c
new file mode 100644
index 0000000..5c0a174
--- /dev/null
+++ b/src/stasis/anastasis-dbinit.c
@@ -0,0 +1,112 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2014, 2015 GNUnet e.V.
+
+ TALER 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.
+
+ TALER 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
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file util/anastasis-dbinit.c
+ * @brief Create tables for the merchant database.
+ * @author Dennis Neufeld
+ * @author Dominik Meister
+ */
+#include "platform.h"
+#include "anastasis_database_lib.h"
+
+
+/**
+ * Return value from main().
+ */
+static int global_ret;
+
+/**
+ * -r option: do full DB reset
+ */
+static int reset_db;
+
+/**
+ * Main function that will be run.
+ *
+ * @param cls closure
+ * @param args remaining command-line arguments
+ * @param cfgfile name of the configuration file used (for saving, can be NULL!)
+ * @param cfg configuration
+ */
+static void
+run (void *cls,
+ char *const *args,
+ const char *cfgfile,
+ const struct GNUNET_CONFIGURATION_Handle *cfg)
+{
+ struct ANASTASIS_DatabasePlugin *plugin;
+
+ if (NULL ==
+ (plugin = ANASTASIS_DB_plugin_load (cfg)))
+ {
+ fprintf (stderr,
+ "Failed to initialize database plugin.\n");
+ global_ret = 1;
+ return;
+ }
+ if (reset_db)
+ {
+ (void) plugin->drop_tables (plugin->cls);
+ ANASTASIS_DB_plugin_unload (plugin);
+ plugin = ANASTASIS_DB_plugin_load (cfg);
+ }
+ ANASTASIS_DB_plugin_unload (plugin);
+}
+
+
+/**
+ * The main function of the database initialization tool.
+ * Used to initialize the Anastasis' database.
+ *
+ * @param argc number of arguments from the command line
+ * @param argv command line arguments
+ * @return 0 ok, 1 on error
+ */
+int
+main (int argc,
+ char *const *argv)
+{
+ struct GNUNET_GETOPT_CommandLineOption options[] = {
+
+ GNUNET_GETOPT_option_flag ('r',
+ "reset",
+ "reset database (DANGEROUS: all existing data is lost!)",
+ &reset_db),
+
+ GNUNET_GETOPT_OPTION_END
+ };
+
+ /* force linker to link against libtalerutil; if we do
+ not do this, the linker may "optimize" libtalerutil
+ away and skip #TALER_OS_init(), which we do need */
+ (void) TALER_project_data_default ();
+ GNUNET_OS_init (ANASTASIS_project_data_default ());
+ GNUNET_assert (GNUNET_OK ==
+ GNUNET_log_setup ("anastasis-dbinit",
+ "INFO",
+ NULL));
+ if (GNUNET_OK !=
+ GNUNET_PROGRAM_run (argc, argv,
+ "anastasis-dbinit",
+ "Initialize anastasis database",
+ options,
+ &run, NULL))
+ return 1;
+ return global_ret;
+}
+
+
+/* end of anastasis-dbinit.c */
diff --git a/src/stasis/anastasis_db_plugin.c b/src/stasis/anastasis_db_plugin.c
new file mode 100644
index 0000000..6b5332c
--- /dev/null
+++ b/src/stasis/anastasis_db_plugin.c
@@ -0,0 +1,146 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2015, 2016 GNUnet e.V. and INRIA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER 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
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file merchantdb/merchantdb_plugin.c
+ * @brief Logic to load database plugin
+ * @author Christian Grothoff
+ * @author Sree Harsha Totakura <sreeharsha@totakura.in>
+ */
+#include "platform.h"
+#include "anastasis_database_plugin.h"
+#include <ltdl.h>
+
+
+/**
+ * Initialize the plugin.
+ *
+ * @param cfg configuration to use
+ * @return #GNUNET_OK on success
+ */
+struct ANASTASIS_DatabasePlugin *
+ANASTASIS_DB_plugin_load (const struct GNUNET_CONFIGURATION_Handle *cfg)
+{
+ char *plugin_name;
+ char *lib_name;
+ struct ANASTASIS_DatabasePlugin *plugin;
+
+ if (GNUNET_SYSERR ==
+ GNUNET_CONFIGURATION_get_value_string (cfg,
+ "anastasis",
+ "db",
+ &plugin_name))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ "anastasis",
+ "db");
+ return NULL;
+ }
+ (void) GNUNET_asprintf (&lib_name,
+ "libanastasis_plugin_db_%s",
+ plugin_name);
+ GNUNET_free (plugin_name);
+ plugin = GNUNET_PLUGIN_load (lib_name,
+ (void *) cfg);
+ if (NULL != plugin)
+ plugin->library_name = lib_name;
+ else
+ lib_name = NULL;
+ return plugin;
+}
+
+
+/**
+ * Shutdown the plugin.
+ *
+ * @param plugin the plugin to unload
+ */
+void
+ANASTASIS_DB_plugin_unload (struct ANASTASIS_DatabasePlugin *plugin)
+{
+ char *lib_name;
+
+ if (NULL == plugin)
+ return;
+ lib_name = plugin->library_name;
+ GNUNET_assert (NULL == GNUNET_PLUGIN_unload (lib_name,
+ plugin));
+ GNUNET_free (lib_name);
+}
+
+
+/**
+ * Libtool search path before we started.
+ */
+static char *old_dlsearchpath;
+
+
+/**
+ * Setup libtool paths.
+ */
+void __attribute__ ((constructor))
+plugin_init ()
+{
+ int err;
+ const char *opath;
+ char *path;
+ char *cpath;
+
+ err = lt_dlinit ();
+ if (err > 0)
+ {
+ fprintf (stderr,
+ _ ("Initialization of plugin mechanism failed: %s!\n"),
+ lt_dlerror ());
+ return;
+ }
+ opath = lt_dlgetsearchpath ();
+ if (NULL != opath)
+ old_dlsearchpath = GNUNET_strdup (opath);
+ path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_LIBDIR);
+ if (NULL != path)
+ {
+ if (NULL != opath)
+ {
+ GNUNET_asprintf (&cpath, "%s:%s", opath, path);
+ lt_dlsetsearchpath (cpath);
+ GNUNET_free (path);
+ GNUNET_free (cpath);
+ }
+ else
+ {
+ lt_dlsetsearchpath (path);
+ GNUNET_free (path);
+ }
+ }
+}
+
+
+/**
+ * Shutdown libtool.
+ */
+void __attribute__ ((destructor))
+plugin_fini ()
+{
+ lt_dlsetsearchpath (old_dlsearchpath);
+ if (NULL != old_dlsearchpath)
+ {
+ GNUNET_free (old_dlsearchpath);
+ }
+ lt_dlexit ();
+}
+
+
+/* end of anastasis_db_plugin.c */
diff --git a/src/stasis/anastasis_db_postgres.conf b/src/stasis/anastasis_db_postgres.conf
new file mode 100644
index 0000000..71a21ac
--- /dev/null
+++ b/src/stasis/anastasis_db_postgres.conf
@@ -0,0 +1,7 @@
+[anastasis]
+#The DB plugin to use
+DB = postgres
+
+[stasis-postgres]
+#The connection string the plugin has to use for connecting to the database
+CONFIG = postgres:///anastasis
diff --git a/src/stasis/drop0001.sql b/src/stasis/drop0001.sql
new file mode 100644
index 0000000..afc457d
--- /dev/null
+++ b/src/stasis/drop0001.sql
@@ -0,0 +1,37 @@
+--
+-- This file is part of ANASTASIS
+-- Copyright (C) 2014--2020 Anastasis Systems SA
+--
+-- 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. If not, see <http://www.gnu.org/licenses/>
+--
+
+-- Everything in one big transaction
+BEGIN;
+
+-- This script DROPs all of the tables we create.
+--
+-- Unlike the other SQL files, it SHOULD be updated to reflect the
+-- latest requirements for dropping tables.
+
+-- Drops for 0001.sql
+DROP TABLE IF EXISTS anastasis_truth CASCADE;
+DROP TABLE IF EXISTS anastasis_user CASCADE;
+DROP TABLE IF EXISTS anastasis_recdoc_payment;
+DROP TABLE IF EXISTS anastasis_recoverydocument;
+DROP TABLE IF EXISTS anastasis_challengecode;
+DROP TABLE IF EXISTS anastasis_challenge_payment;
+
+-- Unregister patch (0001.sql)
+SELECT _v.unregister_patch('stasis-0001');
+
+-- And we're out of here...
+COMMIT;
diff --git a/src/stasis/plugin_anastasis_postgres.c b/src/stasis/plugin_anastasis_postgres.c
new file mode 100644
index 0000000..4aba97c
--- /dev/null
+++ b/src/stasis/plugin_anastasis_postgres.c
@@ -0,0 +1,2301 @@
+/*
+ This file is part of Anastasis
+ Copyright (C) 2020, 2021 Taler Systems SA
+
+ Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file anastasis/plugin_anastasisdb_postgres.c
+ * @brief database helper functions for postgres used by the anastasis
+ * @author Sree Harsha Totakura <sreeharsha@totakura.in>
+ * @author Christian Grothoff
+ * @author Marcello Stanisci
+ */
+#include "platform.h"
+#include "anastasis_database_plugin.h"
+#include "anastasis_database_lib.h"
+#include <taler/taler_pq_lib.h>
+
+/**
+ * How long do we keep transient accounts open (those that have
+ * not been paid at all, but are awaiting payment). This puts
+ * a cap on how long users have to make a payment after a payment
+ * request was generated.
+ */
+#define TRANSIENT_LIFETIME GNUNET_TIME_UNIT_WEEKS
+
+/**
+ * How often do we re-try if we run into a DB serialization error?
+ */
+#define MAX_RETRIES 3
+
+
+/**
+ * Type of the "cls" argument given to each of the functions in
+ * our API.
+ */
+struct PostgresClosure
+{
+
+ /**
+ * Postgres connection handle.
+ */
+ struct GNUNET_PQ_Context *conn;
+
+ /**
+ * Underlying configuration.
+ */
+ const struct GNUNET_CONFIGURATION_Handle *cfg;
+
+ /**
+ * Name of the currently active transaction, NULL if none is active.
+ */
+ const char *transaction_name;
+
+ /**
+ * Currency we accept payments in.
+ */
+ char *currency;
+
+};
+
+
+/**
+ * Drop anastasis tables
+ *
+ * @param cls closure our `struct Plugin`
+ * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure
+ */
+static int
+postgres_drop_tables (void *cls)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_Context *conn;
+
+ conn = GNUNET_PQ_connect_with_cfg (pg->cfg,
+ "stasis-postgres",
+ "drop",
+ NULL,
+ NULL);
+ if (NULL == conn)
+ return GNUNET_SYSERR;
+ GNUNET_PQ_disconnect (conn);
+ return GNUNET_OK;
+}
+
+
+/**
+ * Check that the database connection is still up.
+ *
+ * @param pg connection to check
+ */
+static void
+check_connection (void *cls)
+{
+ struct PostgresClosure *pg = cls;
+
+ GNUNET_PQ_reconnect_if_down (pg->conn);
+}
+
+
+/**
+ * Do a pre-flight check that we are not in an uncommitted transaction.
+ * If we are, try to commit the previous transaction and output a warning.
+ * Does not return anything, as we will continue regardless of the outcome.
+ *
+ * @param cls the `struct PostgresClosure` with the plugin-specific state
+ */
+static void
+postgres_preflight (void *cls)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_ExecuteStatement es[] = {
+ GNUNET_PQ_make_execute ("COMMIT"),
+ GNUNET_PQ_EXECUTE_STATEMENT_END
+ };
+
+ if (NULL == pg->transaction_name)
+ return; /* all good */
+ if (GNUNET_OK ==
+ GNUNET_PQ_exec_statements (pg->conn,
+ es))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "BUG: Preflight check committed transaction `%s'!\n",
+ pg->transaction_name);
+ }
+ else
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "BUG: Preflight check failed to commit transaction `%s'!\n",
+ pg->transaction_name);
+ }
+ pg->transaction_name = NULL;
+}
+
+
+/**
+ * Start a transaction.
+ *
+ * @param cls the `struct PostgresClosure` with the plugin-specific state
+ * @param name unique name identifying the transaction (for debugging),
+ * must point to a constant
+ * @return #GNUNET_OK on success
+ */
+static int
+begin_transaction (void *cls,
+ const char *name)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_ExecuteStatement es[] = {
+ GNUNET_PQ_make_execute ("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
+ GNUNET_PQ_EXECUTE_STATEMENT_END
+ };
+
+ check_connection (pg);
+ postgres_preflight (pg);
+ pg->transaction_name = name;
+ if (GNUNET_OK !=
+ GNUNET_PQ_exec_statements (pg->conn,
+ es))
+ {
+ TALER_LOG_ERROR ("Failed to start transaction\n");
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ return GNUNET_OK;
+}
+
+
+/**
+* Roll back the current transaction of a database connection.
+*
+* @param cls the `struct PostgresClosure` with the plugin-specific state
+* @return #GNUNET_OK on success
+*/
+static void
+rollback (void *cls)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_ExecuteStatement es[] = {
+ GNUNET_PQ_make_execute ("ROLLBACK"),
+ GNUNET_PQ_EXECUTE_STATEMENT_END
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_PQ_exec_statements (pg->conn,
+ es))
+ {
+ TALER_LOG_ERROR ("Failed to rollback transaction\n");
+ GNUNET_break (0);
+ }
+ pg->transaction_name = NULL;
+}
+
+
+/**
+ * Commit the current transaction of a database connection.
+ *
+ * @param cls the `struct PostgresClosure` with the plugin-specific state
+ * @return transaction status code
+ */
+static enum GNUNET_DB_QueryStatus
+commit_transaction (void *cls)
+{
+ struct PostgresClosure *pg = cls;
+ enum GNUNET_DB_QueryStatus qs;
+ struct GNUNET_PQ_QueryParam no_params[] = {
+ GNUNET_PQ_query_param_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "do_commit",
+ no_params);
+ pg->transaction_name = NULL;
+ return qs;
+}
+
+
+/**
+ * Function called to perform "garbage collection" on the
+ * database, expiring records we no longer require. Deletes
+ * all user records that are not paid up (and by cascade deletes
+ * the associated recovery documents). Also deletes expired
+ * truth and financial records older than @a fin_expire.
+ *
+ * @param cls closure
+ * @param expire_backups backups older than the given time stamp should be garbage collected
+ * @param expire_pending_payments payments still pending from since before
+ * this value should be garbage collected
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_gc (void *cls,
+ struct GNUNET_TIME_Absolute expire_backups,
+ struct GNUNET_TIME_Absolute expire_pending_payments)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_absolute_time (&expire_backups),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_QueryParam params2[] = {
+ GNUNET_PQ_query_param_absolute_time (&expire_pending_payments),
+ GNUNET_PQ_query_param_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ postgres_preflight (pg);
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "gc_accounts",
+ params);
+ if (qs < 0)
+ return qs;
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "gc_recdoc_pending_payments",
+ params2);
+}
+
+
+/**
+ * Store encrypted recovery document.
+ *
+ * @param cls closure
+ * @param anastasis_pub public key of the user's account
+ * @param account_sig signature affirming storage request
+ * @param data_hash hash of @a data
+ * @param data contains encrypted_recovery_document
+ * @param data_size size of data blob
+ * @param payment_secret identifier for the payment, used to later charge on uploads
+ * @param[out] version set to the version assigned to the document by the database
+ * @return transaction status, 0 if upload could not be finished because @a payment_secret
+ * did not have enough upload left; HARD error if @a payment_secret is unknown, ...
+ */
+static enum ANASTASIS_DB_StoreStatus
+postgres_store_recovery_document (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub,
+ const struct ANASTASIS_AccountSignatureP *account_sig,
+ const struct GNUNET_HashCode *recovery_data_hash,
+ const void *recovery_data,
+ size_t recovery_data_size,
+ const struct ANASTASIS_PaymentSecretP *payment_secret,
+ uint32_t *version)
+{
+ struct PostgresClosure *pg = cls;
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ postgres_preflight (pg);
+ for (unsigned int retry = 0; retry<MAX_RETRIES; retry++)
+ {
+ if (GNUNET_OK !=
+ begin_transaction (pg,
+ "store_recovery_document"))
+ {
+ GNUNET_break (0);
+ return ANASTASIS_DB_STORE_STATUS_HARD_ERROR;
+ }
+ /* get the current version and hash of the latest recovery document
+ for this account */
+ {
+ struct GNUNET_HashCode dh;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_uint32 ("version",
+ version),
+ GNUNET_PQ_result_spec_auto_from_type ("recovery_data_hash",
+ &dh),
+ GNUNET_PQ_result_spec_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "latest_recovery_version_select",
+ params,
+ rs);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ *version = 1;
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ /* had an existing recovery_data, is it identical? */
+ if (0 == GNUNET_memcmp (&dh,
+ recovery_data_hash))
+ {
+ /* Yes. Previous identical recovery data exists */
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_NO_RESULTS;
+ }
+ (*version)++;
+ break;
+ default:
+ rollback (pg);
+ return qs;
+ }
+ }
+
+ /* First, check if account exists */
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "user_select",
+ params,
+ rs);
+ }
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_PAYMENT_REQUIRED;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ /* handle interesting case below */
+ break;
+ }
+
+ {
+ uint32_t postcounter;
+
+ /* lookup if the user has enough uploads left and decrement */
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_auto_from_type (payment_secret),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_uint32 ("post_counter",
+ &postcounter),
+ GNUNET_PQ_result_spec_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "postcounter_select",
+ params,
+ rs);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
+ }
+
+ if (0 == postcounter)
+ {
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_STORE_LIMIT_EXCEEDED;
+ }
+ /* Decrement the postcounter by one */
+ postcounter--;
+
+ /* Update the postcounter in the Database */
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_uint32 (&postcounter),
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_auto_from_type (payment_secret),
+ GNUNET_PQ_query_param_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "postcounter_update",
+ params);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break (0);
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ default:
+ rollback (pg);
+ return qs;
+ }
+ }
+ }
+
+ /* finally, actually insert the recovery document */
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_uint32 (version),
+ GNUNET_PQ_query_param_auto_from_type (account_sig),
+ GNUNET_PQ_query_param_auto_from_type (recovery_data_hash),
+ GNUNET_PQ_query_param_fixed_size (recovery_data,
+ recovery_data_size),
+ GNUNET_PQ_query_param_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "recovery_document_insert",
+ params);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break (0);
+ rollback (pg);
+ return ANASTASIS_DB_STORE_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ qs = commit_transaction (pg);
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ goto retry;
+ if (qs < 0)
+ return ANASTASIS_DB_STORE_STATUS_HARD_ERROR;
+ return ANASTASIS_DB_STORE_STATUS_SUCCESS;
+ }
+ }
+retry:
+ rollback (pg);
+ }
+ return ANASTASIS_DB_STORE_STATUS_SOFT_ERROR;
+}
+
+
+/**
+ * Increment account lifetime.
+ *
+ * @param cls closure
+ * @param anastasis_pub which account received a payment
+ * @param payment_identifier proof of payment, must be unique and match pending payment
+ * @param lifetime for how long is the account now paid (increment)
+ * @param[out] paid_until set to the end of the lifetime after the operation
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_increment_lifetime (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub,
+ const struct ANASTASIS_PaymentSecretP *payment_identifier,
+ struct GNUNET_TIME_Relative lifetime,
+ struct GNUNET_TIME_Absolute *paid_until)
+{
+ struct PostgresClosure *pg = cls;
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ for (unsigned int retries = 0; retries<MAX_RETRIES; retries++)
+ {
+ if (GNUNET_OK !=
+ begin_transaction (pg,
+ "increment lifetime"))
+ {
+ GNUNET_break (0);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (payment_identifier),
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "recdoc_payment_done",
+ params);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ *paid_until = GNUNET_TIME_UNIT_ZERO_ABS;
+ return qs;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ /* continued below */
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ /* continued below */
+ break;
+ }
+ }
+
+ {
+ enum GNUNET_DB_QueryStatus qs2;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_TIME_Absolute expiration;
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_absolute_time ("expiration_date",
+ &expiration),
+ GNUNET_PQ_result_spec_end
+ };
+
+ qs2 = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "user_select",
+ params,
+ rs);
+ switch (qs2)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ return qs2;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ /* inconsistent, cannot have recdoc payment but no user!? */
+ GNUNET_break (0);
+ rollback (pg);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ else
+ {
+ /* user does not exist, create new one */
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_absolute_time (&expiration),
+ GNUNET_PQ_query_param_end
+ };
+
+ expiration = GNUNET_TIME_relative_to_absolute (lifetime);
+ GNUNET_break (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us !=
+ expiration.abs_value_us);
+ *paid_until = expiration;
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "user_insert",
+ params);
+ }
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ /* existing rec doc payment, return expiration */
+ *paid_until = expiration;
+ rollback (pg);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Payment existed, lifetime of account %s unchanged at %s\n",
+ TALER_B2S (anastasis_pub),
+ GNUNET_STRINGS_absolute_time_to_string (*paid_until));
+ return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
+ }
+ else
+ {
+ /* user exists, update expiration_date */
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_absolute_time (&expiration),
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+
+ expiration = GNUNET_TIME_absolute_add (expiration,
+ lifetime);
+ GNUNET_break (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us !=
+ expiration.abs_value_us);
+ *paid_until = expiration;
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "user_update",
+ params);
+ }
+ break;
+ }
+ }
+
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ return qs;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break (0);
+ rollback (pg);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
+ qs = commit_transaction (pg);
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ goto retry;
+ if (qs < 0)
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Incremented lifetime of account %s to %s\n",
+ TALER_B2S (anastasis_pub),
+ GNUNET_STRINGS_absolute_time_to_string (*paid_until));
+ return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
+retry:
+ rollback (pg);
+ }
+ return GNUNET_DB_STATUS_SOFT_ERROR;
+}
+
+
+/**
+ * Update account lifetime to the maximum of the current
+ * value and @a eol.
+ *
+ * @param cls closure
+ * @param account_pub which account received a payment
+ * @param payment_identifier proof of payment, must be unique and match pending payment
+ * @param eol for how long is the account now paid (absolute)
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_update_lifetime (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub,
+ const struct ANASTASIS_PaymentSecretP *payment_identifier,
+ struct GNUNET_TIME_Absolute eol)
+{
+ struct PostgresClosure *pg = cls;
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ for (unsigned int retries = 0; retries<MAX_RETRIES; retries++)
+ {
+ if (GNUNET_OK !=
+ begin_transaction (pg,
+ "update lifetime"))
+ {
+ GNUNET_break (0);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (payment_identifier),
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "recdoc_payment_done",
+ params);
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ goto retry;
+ if (0 >= qs)
+ {
+ /* same payment made before, or unknown, or error
+ => no further action! */
+ rollback (pg);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Payment existed, lifetime of account %s unchanged\n",
+ TALER_B2S (anastasis_pub));
+ return qs;
+ }
+ }
+
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_TIME_Absolute expiration;
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_absolute_time ("expiration_date",
+ &expiration),
+ GNUNET_PQ_result_spec_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "user_select",
+ params,
+ rs);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ return qs;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ {
+ /* user does not exist, create new one */
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_absolute_time (&eol),
+ GNUNET_PQ_query_param_end
+ };
+
+ GNUNET_break (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us !=
+ eol.abs_value_us);
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "user_insert",
+ params);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Created new account %s with expiration %s\n",
+ TALER_B2S (anastasis_pub),
+ GNUNET_STRINGS_absolute_time_to_string (eol));
+ }
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ {
+ /* user exists, update expiration_date */
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_absolute_time (&expiration),
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+
+ expiration = GNUNET_TIME_absolute_max (expiration,
+ eol);
+ GNUNET_break (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us !=
+ expiration.abs_value_us);
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "user_update",
+ params);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Updated account %s to new expiration %s\n",
+ TALER_B2S (anastasis_pub),
+ GNUNET_STRINGS_absolute_time_to_string (expiration));
+ }
+ break;
+ }
+ }
+
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ return qs;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break (0);
+ rollback (pg);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
+ qs = commit_transaction (pg);
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ goto retry;
+ if (qs < 0)
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
+retry:
+ rollback (pg);
+ }
+ return GNUNET_DB_STATUS_SOFT_ERROR;
+}
+
+
+/**
+ * Store payment. Used to begin a payment, not indicative
+ * that the payment actually was made. (That is done
+ * when we increment the account's lifetime.)
+ *
+ * @param cls closure
+ * @param anastasis_pub anastasis's public key
+ * @param post_counter how many uploads does @a amount pay for
+ * @param payment_secret payment secret which the user must provide with every upload
+ * @param amount how much we asked for
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_record_recdoc_payment (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub,
+ uint32_t post_counter,
+ const struct ANASTASIS_PaymentSecretP *payment_secret,
+ const struct TALER_Amount *amount)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
+ struct GNUNET_TIME_Absolute expiration;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_uint32 (&post_counter),
+ TALER_PQ_query_param_amount (amount),
+ GNUNET_PQ_query_param_auto_from_type (payment_secret),
+ GNUNET_PQ_query_param_absolute_time (&now),
+ GNUNET_PQ_query_param_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ postgres_preflight (pg);
+
+ /* because of constraint at user_id, first we have to verify
+ if user exists, and if not, create one */
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_absolute_time ("expiration_date",
+ &expiration),
+ GNUNET_PQ_result_spec_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "user_select",
+ params,
+ rs);
+ }
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ return qs;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ {
+ /* create new user with short lifetime */
+ struct GNUNET_TIME_Absolute exp
+ = GNUNET_TIME_relative_to_absolute (TRANSIENT_LIFETIME);
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_absolute_time (&exp),
+ GNUNET_PQ_query_param_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "user_insert",
+ params);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break (0);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ /* successful, continue below */
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Created new account %s with transient life until %s\n",
+ TALER_B2S (anastasis_pub),
+ GNUNET_STRINGS_absolute_time_to_string (exp));
+ break;
+ }
+ }
+ /* continue below */
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ /* handle case below */
+ break;
+ }
+
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "recdoc_payment_insert",
+ params);
+}
+
+
+/**
+ * Record truth upload payment was made.
+ *
+ * @param cls closure
+ * @param uuid the truth's UUID
+ * @param amount the amount that was paid
+ * @param duration how long is the truth paid for
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_record_truth_upload_payment (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid,
+ const struct TALER_Amount *amount,
+ struct GNUNET_TIME_Relative duration)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_TIME_Absolute exp = GNUNET_TIME_relative_to_absolute (duration);
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (uuid),
+ TALER_PQ_query_param_amount (amount),
+ GNUNET_PQ_query_param_absolute_time (&exp),
+ GNUNET_PQ_query_param_end
+ };
+
+ check_connection (pg);
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "truth_payment_insert",
+ params);
+}
+
+
+/**
+ * Inquire whether truth upload payment was made.
+ *
+ * @param cls closure
+ * @param uuid the truth's UUID
+ * @param[out] paid_until set for how long this truth is paid for
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_check_truth_upload_paid (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid,
+ struct GNUNET_TIME_Absolute *paid_until)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (uuid),
+ GNUNET_PQ_query_param_absolute_time (&now),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_absolute_time ("expiration",
+ paid_until),
+ GNUNET_PQ_result_spec_end
+ };
+
+ check_connection (pg);
+ return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "truth_payment_select",
+ params,
+ rs);
+}
+
+
+/**
+ * Store payment for challenge.
+ *
+ * @param cls closure
+ * @param truth_key identifier of the challenge to pay
+ * @param payment_secret payment secret which the user must provide with every upload
+ * @param amount how much we asked for
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_record_challenge_payment (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ const struct ANASTASIS_PaymentSecretP *payment_secret,
+ const struct TALER_Amount *amount)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ TALER_PQ_query_param_amount (amount),
+ GNUNET_PQ_query_param_auto_from_type (payment_secret),
+ GNUNET_PQ_query_param_absolute_time (&now),
+ GNUNET_PQ_query_param_end
+ };
+
+ check_connection (pg);
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "challenge_payment_insert",
+ params);
+}
+
+
+/**
+ * Store refund granted for challenge.
+ *
+ * @param cls closure
+ * @param truth_key identifier of the challenge to pay
+ * @param payment_secret payment secret which the user must provide with every upload
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_record_challenge_refund (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ const struct ANASTASIS_PaymentSecretP *payment_secret)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (payment_secret),
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ GNUNET_PQ_query_param_end
+ };
+
+ check_connection (pg);
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "challenge_refund_update",
+ params);
+}
+
+
+/**
+ * Check payment identifier. Used to check if a payment identifier given by
+ * the user is valid (existing and paid).
+ *
+ * @param cls closure
+ * @param payment_secret payment secret which the user must provide with every upload
+ * @param[out] paid bool value to show if payment is paid
+ * @param[out] valid_counter bool value to show if post_counter is > 0
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_check_challenge_payment (
+ void *cls,
+ const struct ANASTASIS_PaymentSecretP *payment_secret,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ bool *paid)
+{
+ struct PostgresClosure *pg = cls;
+ uint8_t paid8;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (payment_secret),
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ GNUNET_PQ_query_param_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_auto_from_type ("paid",
+ &paid8),
+ GNUNET_PQ_result_spec_end
+ };
+
+ check_connection (pg);
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "challenge_payment_select",
+ params,
+ rs);
+ *paid = (0 != paid8);
+ return qs;
+}
+
+
+/**
+ * Check payment identifier. Used to check if a payment identifier given by
+ * the user is valid (existing and paid).
+ *
+ * @param cls closure
+ * @param payment_secret payment secret which the user must provide with every upload
+ * @param[out] paid bool value to show if payment is paid
+ * @param[out] valid_counter bool value to show if post_counter is > 0
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_check_payment_identifier (
+ void *cls,
+ const struct ANASTASIS_PaymentSecretP *payment_secret,
+ bool *paid,
+ bool *valid_counter)
+{
+ struct PostgresClosure *pg = cls;
+ uint32_t counter;
+ uint8_t paid8;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (payment_secret),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_auto_from_type ("paid",
+ &paid8),
+ GNUNET_PQ_result_spec_uint32 ("post_counter",
+ &counter),
+ GNUNET_PQ_result_spec_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "recdoc_payment_select",
+ params,
+ rs);
+
+ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
+ {
+ if (counter > 0)
+ *valid_counter = true;
+ else
+ *valid_counter = false;
+ *paid = (0 != paid8);
+ }
+ return qs;
+}
+
+
+/**
+ * Upload Truth, which contains the Truth and the KeyShare.
+ *
+ * @param cls closure
+ * @param truth_uuid the identifier for the Truth
+ * @param key_share_data contains information of an EncryptedKeyShare
+ * @param method name of method
+ * @param nonce nonce used to compute encryption key for encrypted_truth
+ * @param aes_gcm_tag authentication tag of encrypted_truth
+ * @param encrypted_truth contains the encrypted Truth which includes the ground truth i.e. H(challenge answer), phonenumber, SMS
+ * @param encrypted_truth_size the size of the Truth
+ * @param truth_expiration time till the according data will be stored
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_store_truth (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ const struct ANASTASIS_CRYPTO_EncryptedKeyShareP *key_share_data,
+ const char *mime_type,
+ const void *encrypted_truth,
+ size_t encrypted_truth_size,
+ const char *method,
+ struct GNUNET_TIME_Relative truth_expiration)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_TIME_Absolute expiration = GNUNET_TIME_absolute_get ();
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ GNUNET_PQ_query_param_auto_from_type (key_share_data),
+ GNUNET_PQ_query_param_string (method),
+ GNUNET_PQ_query_param_fixed_size (encrypted_truth,
+ encrypted_truth_size),
+ GNUNET_PQ_query_param_string (mime_type),
+ TALER_PQ_query_param_absolute_time (&expiration),
+ GNUNET_PQ_query_param_end
+ };
+
+
+ expiration = GNUNET_TIME_absolute_add (expiration,
+ truth_expiration);
+ GNUNET_TIME_round_abs (&expiration);
+ check_connection (pg);
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "truth_insert",
+ params);
+}
+
+
+/**
+ * Get the encrypted truth to validate the challenge response
+ *
+ * @param cls closure
+ * @param truth_uuid the identifier for the Truth
+ * @param[out] truth contains the encrypted truth
+ * @param[out] truth_size size of the encrypted truth
+ * @param[out] truth_mime mime type of truth
+ * @param[out] method type of the challenge
+ * @return transaction status
+ */
+enum GNUNET_DB_QueryStatus
+postgres_get_escrow_challenge (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ void **truth,
+ size_t *truth_size,
+ char **truth_mime,
+ char **method)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_variable_size ("encrypted_truth",
+ truth,
+ truth_size),
+ GNUNET_PQ_result_spec_string ("truth_mime",
+ truth_mime),
+ GNUNET_PQ_result_spec_string ("method_name",
+ method),
+ GNUNET_PQ_result_spec_end
+ };
+
+ check_connection (pg);
+ return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "truth_select",
+ params,
+ rs);
+}
+
+
+/**
+ * Lookup (encrypted) key share by @a truth_uuid.
+ *
+ * @param cls closure
+ * @param truth_uuid the identifier for the Truth
+ * @param[out] key_share contains the encrypted Keyshare
+ * @return transaction status
+ */
+enum GNUNET_DB_QueryStatus
+postgres_get_key_share (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ struct ANASTASIS_CRYPTO_EncryptedKeyShareP *key_share)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_auto_from_type ("key_share_data",
+ key_share),
+ GNUNET_PQ_result_spec_end
+ };
+
+ check_connection (pg);
+ return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "key_share_select",
+ params,
+ rs);
+}
+
+
+/**
+ * Check if an account exists, and if so, return the
+ * current @a recovery_document_hash.
+ *
+ * @param cls closure
+ * @param anastasis_pub account identifier
+ * @param[out] paid_until until when is the account paid up?
+ * @param[out] recovery_data_hash set to hash of @a recovery document
+ * @param[out] version set to the recovery policy version
+ * @return transaction status
+ */
+enum ANASTASIS_DB_AccountStatus
+postgres_lookup_account (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub,
+ struct GNUNET_TIME_Absolute *paid_until,
+ struct GNUNET_HashCode *recovery_data_hash,
+ uint32_t *version)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ postgres_preflight (pg);
+ {
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_absolute_time ("expiration_date",
+ paid_until),
+ GNUNET_PQ_result_spec_auto_from_type ("recovery_data_hash",
+ recovery_data_hash),
+ GNUNET_PQ_result_spec_uint32 ("version",
+ version),
+ GNUNET_PQ_result_spec_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "latest_recovery_version_select",
+ params,
+ rs);
+ }
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ return ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ return ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ break; /* handle interesting case below */
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ return ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED;
+ }
+
+ /* check if account exists */
+ {
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_absolute_time ("expiration_date",
+ paid_until),
+ GNUNET_PQ_result_spec_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "user_select",
+ params,
+ rs);
+ }
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ return ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ return ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ /* indicates: no account */
+ return ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ /* indicates: no backup */
+ *version = UINT32_MAX;
+ memset (recovery_data_hash,
+ 0,
+ sizeof (*recovery_data_hash));
+ return ANASTASIS_DB_ACCOUNT_STATUS_NO_RESULTS;
+ default:
+ GNUNET_break (0);
+ return ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR;
+ }
+}
+
+
+/**
+ * Fetch latest recovery document for user.
+ *
+ * @param cls closure
+ * @param anastasis_pub public key of the user's account
+ * @param account_sig signature
+ * @param recovery_data_hash hash of the current recovery data
+ * @param data_size size of data blob
+ * @param data blob which contains the recovery document
+ * @param version[OUT] set to the version number of the policy being returned
+ * @return transaction status
+ */
+enum GNUNET_DB_QueryStatus
+postgres_get_latest_recovery_document (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub,
+ struct ANASTASIS_AccountSignatureP *account_sig,
+ struct GNUNET_HashCode *recovery_data_hash,
+ size_t *data_size,
+ void **data,
+ uint32_t *version)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_uint32 ("version",
+ version),
+ GNUNET_PQ_result_spec_auto_from_type ("account_sig",
+ account_sig),
+ GNUNET_PQ_result_spec_auto_from_type ("recovery_data_hash",
+ recovery_data_hash),
+ GNUNET_PQ_result_spec_variable_size ("recovery_data",
+ data,
+ data_size),
+ GNUNET_PQ_result_spec_end
+ };
+
+ check_connection (pg);
+ postgres_preflight (pg);
+ return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "latest_recoverydocument_select",
+ params,
+ rs);
+}
+
+
+/**
+ * Fetch recovery document for user according given version.
+ *
+ * @param cls closure
+ * @param anastasis_pub public key of the user's account
+ * @param version the version number of the policy the user requests
+ * @param[out] account_sig signature
+ * @param[out] recovery_data_hash hash of the current recovery data
+ * @param[out] data_size size of data blob
+ * @param[out] data blob which contains the recovery document
+ * @return transaction status
+ */
+enum GNUNET_DB_QueryStatus
+postgres_get_recovery_document (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub,
+ uint32_t version,
+ struct ANASTASIS_AccountSignatureP *account_sig,
+ struct GNUNET_HashCode *recovery_data_hash,
+ size_t *data_size,
+ void **data)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (anastasis_pub),
+ GNUNET_PQ_query_param_uint32 (&version),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_auto_from_type ("account_sig",
+ account_sig),
+ GNUNET_PQ_result_spec_auto_from_type ("recovery_data_hash",
+ recovery_data_hash),
+ GNUNET_PQ_result_spec_variable_size ("recovery_data",
+ data,
+ data_size),
+ GNUNET_PQ_result_spec_end
+ };
+
+ check_connection (pg);
+ return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "recoverydocument_select",
+ params,
+ rs);
+}
+
+
+/**
+ * Closure for check_valid_code().
+ */
+struct CheckValidityContext
+{
+ /**
+ * Code to check for.
+ */
+ const struct GNUNET_HashCode *hashed_code;
+
+ /**
+ * Truth we are processing.
+ */
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid;
+
+ /**
+ * Database context.
+ */
+ struct PostgresClosure *pg;
+
+ /**
+ * Set to true if a code matching @e hashed_code was found.
+ */
+ bool valid;
+
+ /**
+ * Set to true if we had a database failure.
+ */
+ bool db_failure;
+
+};
+
+
+/**
+ * Helper function for #postgres_verify_challenge_code().
+ * To be called with the results of a SELECT statement
+ * that has returned @a num_results results.
+ *
+ * @param cls closure of type `struct CheckValidityContext *`
+ * @param result the postgres result
+ * @param num_result the number of results in @a result
+ */
+static void
+check_valid_code (void *cls,
+ PGresult *result,
+ unsigned int num_results)
+{
+ struct CheckValidityContext *cvc = cls;
+ struct PostgresClosure *pg = cvc->pg;
+
+ for (unsigned int i = 0; i < num_results; i++)
+ {
+ uint64_t server_code;
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_uint64 ("code",
+ &server_code),
+ GNUNET_PQ_result_spec_end
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_PQ_extract_result (result,
+ rs,
+ i))
+ {
+ GNUNET_break (0);
+ cvc->db_failure = true;
+ return;
+ }
+ {
+ struct GNUNET_HashCode shashed_code;
+
+ ANASTASIS_hash_answer (server_code,
+ &shashed_code);
+ if (0 ==
+ GNUNET_memcmp (&shashed_code,
+ cvc->hashed_code))
+ {
+ cvc->valid = true;
+ }
+ else
+ {
+ /* count failures to prevent brute-force attacks */
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (cvc->truth_uuid),
+ GNUNET_PQ_query_param_uint64 (&server_code),
+ GNUNET_PQ_query_param_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "challengecode_update_retry",
+ params);
+ if (qs <= 0)
+ {
+ GNUNET_break (0);
+ cvc->db_failure = true;
+ }
+ }
+ }
+ }
+}
+
+
+/**
+ * Verify the provided code with the code on the server.
+ * If the code matches the function will return with success, if the code
+ * does not match, the retry counter will be decreased by one.
+ *
+ * @param cls closure
+ * @param truth_pub identification of the challenge which the code corresponds to
+ * @param hashed_code code which the user provided and wants to verify
+ * @return code validity status
+ */
+enum ANASTASIS_DB_CodeStatus
+postgres_verify_challenge_code (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ const struct GNUNET_HashCode *hashed_code)
+{
+ struct PostgresClosure *pg = cls;
+ struct CheckValidityContext cvc = {
+ .truth_uuid = truth_uuid,
+ .hashed_code = hashed_code,
+ .pg = pg
+ };
+ struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ TALER_PQ_query_param_absolute_time (&now),
+ GNUNET_PQ_query_param_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ GNUNET_TIME_round_abs (&now);
+ qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
+ "challengecode_select",
+ params,
+ &check_valid_code,
+ &cvc);
+ if ( (qs < 0) ||
+ (cvc.db_failure) )
+ return ANASTASIS_DB_CODE_STATUS_HARD_ERROR;
+ if (cvc.valid)
+ return ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED;
+ if (0 == qs)
+ return ANASTASIS_DB_CODE_STATUS_NO_RESULTS;
+ return ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH;
+}
+
+
+/**
+ * Lookup pending payment for a certain challenge.
+ *
+ * @param cls closure
+ * @param truth_uuid identification of the challenge
+ * @param[out] payment_secret set to the challenge payment secret
+ * @return transaction status
+ */
+enum GNUNET_DB_QueryStatus
+postgres_lookup_challenge_payment (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ struct ANASTASIS_PaymentSecretP *payment_secret)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
+ struct GNUNET_TIME_Absolute recent
+ = GNUNET_TIME_absolute_subtract (now,
+ ANASTASIS_CHALLENGE_OFFER_LIFETIME);
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ GNUNET_PQ_query_param_absolute_time (&recent),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_auto_from_type ("payment_identifier",
+ payment_secret),
+ GNUNET_PQ_result_spec_end
+ };
+
+ return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "challenge_pending_payment_select",
+ params,
+ rs);
+}
+
+
+/**
+ * Update payment status of challenge
+ *
+ * @param cls closure
+ * @param truth_uuid which challenge received a payment
+ * @param payment_identifier proof of payment, must be unique and match pending payment
+ * @return transaction status
+ */
+enum GNUNET_DB_QueryStatus
+postgres_update_challenge_payment (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ const struct ANASTASIS_PaymentSecretP *payment_identifier)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (payment_identifier),
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ GNUNET_PQ_query_param_end
+ };
+
+ check_connection (pg);
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "challenge_payment_done",
+ params);
+}
+
+
+/**
+ * Create a new challenge code for a given challenge identified by the challenge
+ * public key. The function will first check if there is already a valid code
+ * for this challenge present and won't insert a new one in this case.
+ *
+ * @param cls closure
+ * @param truth_uuid the identifier for the challenge
+ * @param rotation_period for how long is the code available
+ * @param validity_period for how long is the code available
+ * @param retry_counter amount of retries allowed
+ * @param[out] retransmission_date when to next retransmit
+ * @param[out] code set to the code which will be checked for later
+ * @return transaction status,
+ * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if we are out of valid tries,
+ * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if @a code is now in the DB
+ */
+enum GNUNET_DB_QueryStatus
+postgres_create_challenge_code (
+ void *cls,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ struct GNUNET_TIME_Relative rotation_period,
+ struct GNUNET_TIME_Relative validity_period,
+ unsigned int retry_counter,
+ struct GNUNET_TIME_Absolute *retransmission_date,
+ uint64_t *code)
+{
+ struct PostgresClosure *pg = cls;
+ enum GNUNET_DB_QueryStatus qs;
+ struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
+ struct GNUNET_TIME_Absolute expiration_date;
+ struct GNUNET_TIME_Absolute ex_rot;
+
+ check_connection (pg);
+ GNUNET_TIME_round_abs (&now);
+ expiration_date = GNUNET_TIME_absolute_add (now,
+ validity_period);
+ ex_rot = GNUNET_TIME_absolute_subtract (now,
+ rotation_period);
+ for (unsigned int retries = 0; retries<MAX_RETRIES; retries++)
+ {
+ if (GNUNET_OK !=
+ begin_transaction (pg,
+ "create_challenge_code"))
+ {
+ GNUNET_break (0);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ {
+ uint32_t old_retry_counter;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ TALER_PQ_query_param_absolute_time (&now),
+ TALER_PQ_query_param_absolute_time (&ex_rot),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_uint64 ("code",
+ code),
+ GNUNET_PQ_result_spec_uint32 ("retry_counter",
+ &old_retry_counter),
+ GNUNET_PQ_result_spec_absolute_time ("retransmission_date",
+ retransmission_date),
+ GNUNET_PQ_result_spec_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "challengecode_select_meta",
+ params,
+ rs);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ rollback (pg);
+ return qs;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ /* no active challenge, create fresh one (below) */
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ if (0 == old_retry_counter)
+ {
+ rollback (pg);
+ return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
+ }
+ rollback (pg);
+ return qs;
+ }
+ }
+
+ *code = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE,
+ INT64_MAX);
+ *retransmission_date = GNUNET_TIME_UNIT_ZERO_ABS;
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ GNUNET_PQ_query_param_uint64 (code),
+ TALER_PQ_query_param_absolute_time (&now),
+ TALER_PQ_query_param_absolute_time (&expiration_date),
+ GNUNET_PQ_query_param_uint32 (&retry_counter),
+ GNUNET_PQ_query_param_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "challengecode_insert",
+ params);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ rollback (pg);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ goto retry;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break (0);
+ rollback (pg);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
+ }
+ qs = commit_transaction (pg);
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ goto retry;
+ if (qs < 0)
+ return qs;
+ return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
+retry:
+ rollback (pg);
+ }
+ return GNUNET_DB_STATUS_SOFT_ERROR;
+}
+
+
+/**
+ * Remember in the database that we successfully sent a challenge.
+ *
+ * @param cls closure
+ * @param payment_secret payment secret which the user must provide with every upload
+ * @param truth_uuid the identifier for the challenge
+ * @param code the challenge that was sent
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_mark_challenge_sent (
+ void *cls,
+ const struct ANASTASIS_PaymentSecretP *payment_secret,
+ const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
+ uint64_t code)
+{
+ struct PostgresClosure *pg = cls;
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ {
+ struct GNUNET_TIME_Absolute now;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ GNUNET_PQ_query_param_uint64 (&code),
+ TALER_PQ_query_param_absolute_time (&now),
+ GNUNET_PQ_query_param_end
+ };
+
+ now = GNUNET_TIME_absolute_get ();
+ GNUNET_TIME_round_abs (&now);
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "challengecode_mark_sent",
+ params);
+ if (qs <= 0)
+ return qs;
+ }
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ GNUNET_PQ_query_param_auto_from_type (payment_secret),
+ GNUNET_PQ_query_param_end
+ };
+
+ qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "challengepayment_dec_counter",
+ params);
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* probably was free */
+ return qs;
+ }
+}
+
+
+/**
+ * Function called to remove all expired codes from the database.
+ * FIXME maybe implemented as part of postgres_gc() in the future.
+ *
+ * @return transaction status
+ */
+enum GNUNET_DB_QueryStatus
+postgres_challenge_gc (void *cls)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_TIME_Absolute time_now = GNUNET_TIME_absolute_get ();
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_absolute_time (&time_now),
+ GNUNET_PQ_query_param_end
+ };
+
+ check_connection (pg);
+ postgres_preflight (pg);
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "gc_challengecodes",
+ params);
+}
+
+
+/**
+ * Initialize Postgres database subsystem.
+ *
+ * @param cls a configuration instance
+ * @return NULL on error, otherwise a `struct TALER_ANASTASISDB_Plugin`
+ */
+void *
+libanastasis_plugin_db_postgres_init (void *cls)
+{
+ struct GNUNET_CONFIGURATION_Handle *cfg = cls;
+ struct PostgresClosure *pg;
+ struct ANASTASIS_DatabasePlugin *plugin;
+ struct GNUNET_PQ_PreparedStatement ps[] = {
+ GNUNET_PQ_make_prepare ("user_insert",
+ "INSERT INTO anastasis_user "
+ "(user_id"
+ ",expiration_date"
+ ") VALUES "
+ "($1, $2);",
+ 2),
+ GNUNET_PQ_make_prepare ("do_commit",
+ "COMMIT",
+ 0),
+ GNUNET_PQ_make_prepare ("user_select",
+ "SELECT"
+ " expiration_date "
+ "FROM anastasis_user"
+ " WHERE user_id=$1"
+ " FOR UPDATE;",
+ 1),
+ GNUNET_PQ_make_prepare ("user_update",
+ "UPDATE anastasis_user"
+ " SET "
+ " expiration_date=$1"
+ " WHERE user_id=$2;",
+ 2),
+ GNUNET_PQ_make_prepare ("recdoc_payment_insert",
+ "INSERT INTO anastasis_recdoc_payment "
+ "(user_id"
+ ",post_counter"
+ ",amount_val"
+ ",amount_frac"
+ ",payment_identifier"
+ ",creation_date"
+ ") VALUES "
+ "($1, $2, $3, $4, $5, $6);",
+ 6),
+ GNUNET_PQ_make_prepare ("challenge_payment_insert",
+ "INSERT INTO anastasis_challenge_payment "
+ "(truth_uuid"
+ ",amount_val"
+ ",amount_frac"
+ ",payment_identifier"
+ ",creation_date"
+ ") VALUES "
+ "($1, $2, $3, $4, $5);",
+ 5),
+ GNUNET_PQ_make_prepare ("truth_payment_insert",
+ "INSERT INTO anastasis_truth_payment "
+ "(truth_uuid"
+ ",amount_val"
+ ",amount_frac"
+ ",expiration"
+ ") VALUES "
+ "($1, $2, $3, $4);",
+ 4),
+ GNUNET_PQ_make_prepare ("recdoc_payment_done",
+ "UPDATE anastasis_recdoc_payment "
+ "SET"
+ " paid=TRUE "
+ "WHERE"
+ " payment_identifier=$1"
+ " AND"
+ " user_id=$2"
+ " AND"
+ " paid=FALSE;",
+ 2),
+ GNUNET_PQ_make_prepare ("challenge_refund_update",
+ "UPDATE anastasis_challenge_payment "
+ "SET"
+ " refunded=TRUE "
+ "WHERE"
+ " payment_identifier=$1"
+ " AND"
+ " paid=TRUE"
+ " AND"
+ " truth_uuid=$2;",
+ 2),
+ GNUNET_PQ_make_prepare ("challenge_payment_done",
+ "UPDATE anastasis_challenge_payment "
+ "SET"
+ " paid=TRUE "
+ "WHERE"
+ " payment_identifier=$1"
+ " AND"
+ " refunded=FALSE"
+ " AND"
+ " truth_uuid=$2"
+ " AND"
+ " paid=FALSE;",
+ 2),
+ GNUNET_PQ_make_prepare ("recdoc_payment_select",
+ "SELECT"
+ " creation_date"
+ ",post_counter"
+ ",amount_val"
+ ",amount_frac"
+ ",paid"
+ " FROM anastasis_recdoc_payment"
+ " WHERE payment_identifier=$1;",
+ 1),
+ GNUNET_PQ_make_prepare ("truth_payment_select",
+ "SELECT"
+ " expiration"
+ " FROM anastasis_truth_payment"
+ " WHERE truth_uuid=$1"
+ " AND expiration>$2;",
+ 2),
+ GNUNET_PQ_make_prepare ("challenge_payment_select",
+ "SELECT"
+ " creation_date"
+ ",amount_val"
+ ",amount_frac"
+ ",paid"
+ " FROM anastasis_challenge_payment"
+ " WHERE payment_identifier=$1"
+ " AND truth_uuid=$2"
+ " AND refunded=FALSE"
+ " AND counter>0;",
+ 1),
+ GNUNET_PQ_make_prepare ("challenge_pending_payment_select",
+ "SELECT"
+ " creation_date"
+ ",payment_identifier"
+ ",amount_val"
+ ",amount_frac"
+ " FROM anastasis_challenge_payment"
+ " WHERE"
+ " paid=FALSE"
+ " AND"
+ " refunded=FALSE"
+ " AND"
+ " truth_uuid=$1"
+ " AND"
+ " creation_date > $2;",
+ 1),
+ GNUNET_PQ_make_prepare ("recdoc_payments_select",
+ "SELECT"
+ " user_id"
+ ",payment_identifier"
+ ",amount_val"
+ ",amount_frac"
+ " FROM anastasis_recdoc_payment"
+ " WHERE paid=FALSE;",
+ 0),
+ GNUNET_PQ_make_prepare ("gc_accounts",
+ "DELETE FROM anastasis_user "
+ "WHERE"
+ " expiration_date < $1;",
+ 1),
+ GNUNET_PQ_make_prepare ("gc_recdoc_pending_payments",
+ "DELETE FROM anastasis_recdoc_payment "
+ "WHERE"
+ " paid=FALSE"
+ " AND"
+ " creation_date < $1;",
+ 1),
+ GNUNET_PQ_make_prepare ("gc_challenge_pending_payments",
+ "DELETE FROM anastasis_challenge_payment "
+ "WHERE"
+ " (paid=FALSE"
+ " OR"
+ " refunded=TRUE)"
+ " AND"
+ " creation_date < $1;",
+ 1),
+ GNUNET_PQ_make_prepare ("truth_insert",
+ "INSERT INTO anastasis_truth "
+ "(truth_uuid"
+ ",key_share_data"
+ ",method_name"
+ ",encrypted_truth"
+ ",truth_mime"
+ ",expiration"
+ ") VALUES "
+ "($1, $2, $3, $4, $5, $6);",
+ 6),
+ GNUNET_PQ_make_prepare ("recovery_document_insert",
+ "INSERT INTO anastasis_recoverydocument "
+ "(user_id"
+ ",version"
+ ",account_sig"
+ ",recovery_data_hash"
+ ",recovery_data"
+ ") VALUES "
+ "($1, $2, $3, $4, $5);",
+ 5),
+ GNUNET_PQ_make_prepare ("truth_select",
+ "SELECT "
+ " method_name"
+ ",encrypted_truth"
+ ",truth_mime"
+ " FROM anastasis_truth"
+ " WHERE truth_uuid =$1;",
+ 1),
+ GNUNET_PQ_make_prepare ("latest_recoverydocument_select",
+ "SELECT "
+ " version"
+ ",account_sig"
+ ",recovery_data_hash"
+ ",recovery_data"
+ " FROM anastasis_recoverydocument"
+ " WHERE user_id =$1 "
+ " ORDER BY version DESC"
+ " LIMIT 1;",
+ 1),
+ GNUNET_PQ_make_prepare ("latest_recovery_version_select",
+ "SELECT"
+ " version"
+ ",recovery_data_hash"
+ ",expiration_date"
+ " FROM anastasis_recoverydocument"
+ " JOIN anastasis_user USING (user_id)"
+ " WHERE user_id=$1"
+ " ORDER BY version DESC"
+ " LIMIT 1;",
+ 1),
+ GNUNET_PQ_make_prepare ("recoverydocument_select",
+ "SELECT "
+ " account_sig"
+ ",recovery_data_hash"
+ ",recovery_data"
+ " FROM anastasis_recoverydocument"
+ " WHERE user_id=$1"
+ " AND version=$2;",
+ 2),
+ GNUNET_PQ_make_prepare ("postcounter_select",
+ "SELECT"
+ " post_counter"
+ " FROM anastasis_recdoc_payment"
+ " WHERE user_id=$1"
+ " AND payment_identifier=$2;",
+ 2),
+ GNUNET_PQ_make_prepare ("postcounter_update",
+ "UPDATE "
+ "anastasis_recdoc_payment "
+ "SET "
+ "post_counter=$1 "
+ "WHERE user_id =$2 "
+ "AND payment_identifier=$3;",
+ 3),
+ GNUNET_PQ_make_prepare ("key_share_select",
+ "SELECT "
+ "key_share_data "
+ "FROM "
+ "anastasis_truth "
+ "WHERE truth_uuid =$1;",
+ 1),
+ GNUNET_PQ_make_prepare ("challengecode_insert",
+ "INSERT INTO anastasis_challengecode "
+ "(truth_uuid"
+ ",code"
+ ",creation_date"
+ ",expiration_date"
+ ",retry_counter"
+ ") VALUES "
+ "($1, $2, $3, $4, $5);",
+ 5),
+ GNUNET_PQ_make_prepare ("challengecode_select",
+ "SELECT "
+ " code"
+ " FROM anastasis_challengecode"
+ " WHERE truth_uuid=$1"
+ " AND expiration_date > $2"
+ " AND retry_counter > 0;",
+ 2),
+ GNUNET_PQ_make_prepare ("challengecode_select_meta",
+ "SELECT "
+ " code"
+ ",retry_counter"
+ ",retransmission_date"
+ " FROM anastasis_challengecode"
+ " WHERE truth_uuid=$1"
+ " AND expiration_date > $2"
+ " AND creation_date > $3"
+ " ORDER BY creation_date DESC"
+ " LIMIT 1;",
+ 2),
+ GNUNET_PQ_make_prepare ("challengecode_update_retry",
+ "UPDATE anastasis_challengecode"
+ " SET retry_counter=retry_counter - 1"
+ " WHERE truth_uuid=$1"
+ " AND code=$2"
+ " AND retry_counter > 0;",
+ 1),
+ GNUNET_PQ_make_prepare ("challengepayment_dec_counter",
+ "UPDATE anastasis_challenge_payment"
+ " SET counter=counter - 1"
+ " WHERE truth_uuid=$1"
+ " AND payment_identifier=$2"
+ " AND counter > 0;",
+ 2),
+ GNUNET_PQ_make_prepare ("challengecode_mark_sent",
+ "UPDATE anastasis_challengecode"
+ " SET retransmission_date=$3"
+ " WHERE truth_uuid=$1"
+ " AND code=$2"
+ " AND creation_date IN"
+ " (SELECT creation_date"
+ " FROM anastasis_challengecode"
+ " WHERE truth_uuid=$1"
+ " AND code=$2"
+ " ORDER BY creation_date DESC"
+ " LIMIT 1);",
+ 3),
+ GNUNET_PQ_make_prepare ("gc_challengecodes",
+ "DELETE FROM anastasis_challengecode "
+ "WHERE "
+ "expiration_date < $1;",
+ 1),
+ GNUNET_PQ_PREPARED_STATEMENT_END
+ };
+
+ pg = GNUNET_new (struct PostgresClosure);
+ pg->cfg = cfg;
+ pg->conn = GNUNET_PQ_connect_with_cfg (cfg,
+ "stasis-postgres",
+ "stasis-",
+ NULL,
+ ps);
+ if (NULL == pg->conn)
+ {
+ GNUNET_free (pg);
+ return NULL;
+ }
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_string (cfg,
+ "taler",
+ "CURRENCY",
+ &pg->currency))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ "taler",
+ "CURRENCY");
+ GNUNET_PQ_disconnect (pg->conn);
+ GNUNET_free (pg);
+ return NULL;
+ }
+ plugin = GNUNET_new (struct ANASTASIS_DatabasePlugin);
+ plugin->cls = pg;
+ plugin->drop_tables = &postgres_drop_tables;
+ plugin->gc = &postgres_gc;
+ plugin->preflight = &postgres_preflight;
+ plugin->rollback = &rollback;
+ plugin->commit = &commit_transaction;
+ plugin->store_recovery_document = &postgres_store_recovery_document;
+ plugin->record_recdoc_payment = &postgres_record_recdoc_payment;
+ plugin->store_truth = &postgres_store_truth;
+ plugin->get_escrow_challenge = &postgres_get_escrow_challenge;
+ plugin->get_key_share = &postgres_get_key_share;
+ plugin->get_latest_recovery_document = &postgres_get_latest_recovery_document;
+ plugin->get_recovery_document = &postgres_get_recovery_document;
+ plugin->lookup_account = &postgres_lookup_account;
+ plugin->check_payment_identifier = &postgres_check_payment_identifier;
+ plugin->increment_lifetime = &postgres_increment_lifetime;
+ plugin->update_lifetime = &postgres_update_lifetime;
+ plugin->start = &begin_transaction;
+ plugin->check_connection = &check_connection;
+ plugin->verify_challenge_code = &postgres_verify_challenge_code;
+ plugin->create_challenge_code = &postgres_create_challenge_code;
+ plugin->mark_challenge_sent = &postgres_mark_challenge_sent;
+ plugin->challenge_gc = &postgres_challenge_gc;
+ plugin->record_truth_upload_payment = &postgres_record_truth_upload_payment;
+ plugin->check_truth_upload_paid = &postgres_check_truth_upload_paid;
+ plugin->record_challenge_payment = &postgres_record_challenge_payment;
+ plugin->record_challenge_refund = &postgres_record_challenge_refund;
+ plugin->check_challenge_payment = &postgres_check_challenge_payment;
+ plugin->lookup_challenge_payment = &postgres_lookup_challenge_payment;
+ plugin->update_challenge_payment = &postgres_update_challenge_payment;
+ return plugin;
+}
+
+
+/**
+ * Shutdown Postgres database subsystem.
+ *
+ * @param cls a `struct ANASTASIS_DB_STATUS_Plugin`
+ * @return NULL (always)
+ */
+void *
+libanastasis_plugin_db_postgres_done (void *cls)
+{
+ struct ANASTASIS_DatabasePlugin *plugin = cls;
+ struct PostgresClosure *pg = plugin->cls;
+
+ GNUNET_PQ_disconnect (pg->conn);
+ GNUNET_free (pg->currency);
+ GNUNET_free (pg);
+ GNUNET_free (plugin);
+ return NULL;
+}
+
+
+/* end of plugin_anastasisdb_postgres.c */
diff --git a/src/stasis/stasis-0000.sql b/src/stasis/stasis-0000.sql
new file mode 100644
index 0000000..116f409
--- /dev/null
+++ b/src/stasis/stasis-0000.sql
@@ -0,0 +1,293 @@
+-- LICENSE AND COPYRIGHT
+--
+-- Copyright (C) 2010 Hubert depesz Lubaczewski
+--
+-- This program is distributed under the (Revised) BSD License:
+-- L<http://www.opensource.org/licenses/bsd-license.php>
+--
+-- Redistribution and use in source and binary forms, with or without
+-- modification, are permitted provided that the following conditions
+-- are met:
+--
+-- * Redistributions of source code must retain the above copyright
+-- notice, this list of conditions and the following disclaimer.
+--
+-- * Redistributions in binary form must reproduce the above copyright
+-- notice, this list of conditions and the following disclaimer in the
+-- documentation and/or other materials provided with the distribution.
+--
+-- * Neither the name of Hubert depesz Lubaczewski's Organization
+-- nor the names of its contributors may be used to endorse or
+-- promote products derived from this software without specific
+-- prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+-- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+-- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+-- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--
+-- Code origin: https://gitlab.com/depesz/Versioning/blob/master/install.versioning.sql
+--
+--
+-- # NAME
+--
+-- **Versioning** - simplistic take on tracking and applying changes to databases.
+--
+-- # DESCRIPTION
+--
+-- This project strives to provide simple way to manage changes to
+-- database.
+--
+-- Instead of making changes on development server, then finding
+-- differences between production and development, deciding which ones
+-- should be installed on production, and finding a way to install them -
+-- you start with writing diffs themselves!
+--
+-- # INSTALLATION
+--
+-- To install versioning simply run install.versioning.sql in your database
+-- (all of them: production, stage, test, devel, ...).
+--
+-- # USAGE
+--
+-- In your files with patches to database, put whole logic in single
+-- transaction, and use \_v.\* functions - usually \_v.register_patch() at
+-- least to make sure everything is OK.
+--
+-- For example. Let's assume you have patch files:
+--
+-- ## 0001.sql:
+--
+-- ```
+-- create table users (id serial primary key, username text);
+-- ```
+--
+-- ## 0002.sql:
+--
+-- ```
+-- insert into users (username) values ('depesz');
+-- ```
+-- To change it to use versioning you would change the files, to this
+-- state:
+--
+-- 0000.sql:
+--
+-- ```
+-- BEGIN;
+-- select _v.register_patch('000-base', NULL, NULL);
+-- create table users (id serial primary key, username text);
+-- COMMIT;
+-- ```
+--
+-- ## 0002.sql:
+--
+-- ```
+-- BEGIN;
+-- select _v.register_patch('001-users', ARRAY['000-base'], NULL);
+-- insert into users (username) values ('depesz');
+-- COMMIT;
+-- ```
+--
+-- This will make sure that patch 001-users can only be applied after
+-- 000-base.
+--
+-- # AVAILABLE FUNCTIONS
+--
+-- ## \_v.register_patch( TEXT )
+--
+-- Registers named patch, or dies if it is already registered.
+--
+-- Returns integer which is id of patch in \_v.patches table - only if it
+-- succeeded.
+--
+-- ## \_v.register_patch( TEXT, TEXT[] )
+--
+-- Same as \_v.register_patch( TEXT ), but checks is all given patches (given as
+-- array in second argument) are already registered.
+--
+-- ## \_v.register_patch( TEXT, TEXT[], TEXT[] )
+--
+-- Same as \_v.register_patch( TEXT, TEXT[] ), but also checks if there are no conflicts with preexisting patches.
+--
+-- Third argument is array of names of patches that conflict with current one. So
+-- if any of them is installed - register_patch will error out.
+--
+-- ## \_v.unregister_patch( TEXT )
+--
+-- Removes information about given patch from the versioning data.
+--
+-- It doesn't remove objects that were created by this patch - just removes
+-- metainformation.
+--
+-- ## \_v.assert_user_is_superuser()
+--
+-- Make sure that current patch is being loaded by superuser.
+--
+-- If it's not - it will raise exception, and break transaction.
+--
+-- ## \_v.assert_user_is_not_superuser()
+--
+-- Make sure that current patch is not being loaded by superuser.
+--
+-- If it is - it will raise exception, and break transaction.
+--
+-- ## \_v.assert_user_is_one_of(TEXT, TEXT, ... )
+--
+-- Make sure that current patch is being loaded by one of listed users.
+--
+-- If ```current_user``` is not listed as one of arguments - function will raise
+-- exception and break the transaction.
+
+BEGIN;
+
+-- This file adds versioning support to database it will be loaded to.
+-- It requires that PL/pgSQL is already loaded - will raise exception otherwise.
+-- All versioning "stuff" (tables, functions) is in "_v" schema.
+
+-- All functions are defined as 'RETURNS SETOF INT4' to be able to make them to RETURN literally nothing (0 rows).
+-- >> RETURNS VOID<< IS similar, but it still outputs "empty line" in psql when calling.
+CREATE SCHEMA IF NOT EXISTS _v;
+COMMENT ON SCHEMA _v IS 'Schema for versioning data and functionality.';
+
+CREATE TABLE IF NOT EXISTS _v.patches (
+ patch_name TEXT PRIMARY KEY,
+ applied_tsz TIMESTAMPTZ NOT NULL DEFAULT now(),
+ applied_by TEXT NOT NULL,
+ requires TEXT[],
+ conflicts TEXT[]
+);
+COMMENT ON TABLE _v.patches IS 'Contains information about what patches are currently applied on database.';
+COMMENT ON COLUMN _v.patches.patch_name IS 'Name of patch, has to be unique for every patch.';
+COMMENT ON COLUMN _v.patches.applied_tsz IS 'When the patch was applied.';
+COMMENT ON COLUMN _v.patches.applied_by IS 'Who applied this patch (PostgreSQL username)';
+COMMENT ON COLUMN _v.patches.requires IS 'List of patches that are required for given patch.';
+COMMENT ON COLUMN _v.patches.conflicts IS 'List of patches that conflict with given patch.';
+
+CREATE OR REPLACE FUNCTION _v.register_patch( IN in_patch_name TEXT, IN in_requirements TEXT[], in_conflicts TEXT[], OUT versioning INT4 ) RETURNS setof INT4 AS $$
+DECLARE
+ t_text TEXT;
+ t_text_a TEXT[];
+ i INT4;
+BEGIN
+ -- Thanks to this we know only one patch will be applied at a time
+ LOCK TABLE _v.patches IN EXCLUSIVE MODE;
+
+ SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name;
+ IF FOUND THEN
+ RAISE EXCEPTION 'Patch % is already applied!', in_patch_name;
+ END IF;
+
+ t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE patch_name = any( in_conflicts ) );
+ IF array_upper( t_text_a, 1 ) IS NOT NULL THEN
+ RAISE EXCEPTION 'Versioning patches conflict. Conflicting patche(s) installed: %.', array_to_string( t_text_a, ', ' );
+ END IF;
+
+ IF array_upper( in_requirements, 1 ) IS NOT NULL THEN
+ t_text_a := '{}';
+ FOR i IN array_lower( in_requirements, 1 ) .. array_upper( in_requirements, 1 ) LOOP
+ SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_requirements[i];
+ IF NOT FOUND THEN
+ t_text_a := t_text_a || in_requirements[i];
+ END IF;
+ END LOOP;
+ IF array_upper( t_text_a, 1 ) IS NOT NULL THEN
+ RAISE EXCEPTION 'Missing prerequisite(s): %.', array_to_string( t_text_a, ', ' );
+ END IF;
+ END IF;
+
+ INSERT INTO _v.patches (patch_name, applied_tsz, applied_by, requires, conflicts ) VALUES ( in_patch_name, now(), current_user, coalesce( in_requirements, '{}' ), coalesce( in_conflicts, '{}' ) );
+ RETURN;
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[], TEXT[] ) IS 'Function to register patches in database. Raises exception if there are conflicts, prerequisites are not installed or the migration has already been installed.';
+
+CREATE OR REPLACE FUNCTION _v.register_patch( TEXT, TEXT[] ) RETURNS setof INT4 AS $$
+ SELECT _v.register_patch( $1, $2, NULL );
+$$ language sql;
+COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[] ) IS 'Wrapper to allow registration of patches without conflicts.';
+CREATE OR REPLACE FUNCTION _v.register_patch( TEXT ) RETURNS setof INT4 AS $$
+ SELECT _v.register_patch( $1, NULL, NULL );
+$$ language sql;
+COMMENT ON FUNCTION _v.register_patch( TEXT ) IS 'Wrapper to allow registration of patches without requirements and conflicts.';
+
+CREATE OR REPLACE FUNCTION _v.unregister_patch( IN in_patch_name TEXT, OUT versioning INT4 ) RETURNS setof INT4 AS $$
+DECLARE
+ i INT4;
+ t_text_a TEXT[];
+BEGIN
+ -- Thanks to this we know only one patch will be applied at a time
+ LOCK TABLE _v.patches IN EXCLUSIVE MODE;
+
+ t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE in_patch_name = ANY( requires ) );
+ IF array_upper( t_text_a, 1 ) IS NOT NULL THEN
+ RAISE EXCEPTION 'Cannot uninstall %, as it is required by: %.', in_patch_name, array_to_string( t_text_a, ', ' );
+ END IF;
+
+ DELETE FROM _v.patches WHERE patch_name = in_patch_name;
+ GET DIAGNOSTICS i = ROW_COUNT;
+ IF i < 1 THEN
+ RAISE EXCEPTION 'Patch % is not installed, so it can''t be uninstalled!', in_patch_name;
+ END IF;
+
+ RETURN;
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.unregister_patch( TEXT ) IS 'Function to unregister patches in database. Dies if the patch is not registered, or if unregistering it would break dependencies.';
+
+CREATE OR REPLACE FUNCTION _v.assert_patch_is_applied( IN in_patch_name TEXT ) RETURNS TEXT as $$
+DECLARE
+ t_text TEXT;
+BEGIN
+ SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'Patch % is not applied!', in_patch_name;
+ END IF;
+ RETURN format('Patch %s is applied.', in_patch_name);
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_patch_is_applied( TEXT ) IS 'Function that can be used to make sure that patch has been applied.';
+
+CREATE OR REPLACE FUNCTION _v.assert_user_is_superuser() RETURNS TEXT as $$
+DECLARE
+ v_super bool;
+BEGIN
+ SELECT usesuper INTO v_super FROM pg_user WHERE usename = current_user;
+ IF v_super THEN
+ RETURN 'assert_user_is_superuser: OK';
+ END IF;
+ RAISE EXCEPTION 'Current user is not superuser - cannot continue.';
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_user_is_superuser() IS 'Function that can be used to make sure that patch is being applied using superuser account.';
+
+CREATE OR REPLACE FUNCTION _v.assert_user_is_not_superuser() RETURNS TEXT as $$
+DECLARE
+ v_super bool;
+BEGIN
+ SELECT usesuper INTO v_super FROM pg_user WHERE usename = current_user;
+ IF v_super THEN
+ RAISE EXCEPTION 'Current user is superuser - cannot continue.';
+ END IF;
+ RETURN 'assert_user_is_not_superuser: OK';
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_user_is_not_superuser() IS 'Function that can be used to make sure that patch is being applied using normal (not superuser) account.';
+
+CREATE OR REPLACE FUNCTION _v.assert_user_is_one_of(VARIADIC p_acceptable_users TEXT[] ) RETURNS TEXT as $$
+DECLARE
+BEGIN
+ IF current_user = any( p_acceptable_users ) THEN
+ RETURN 'assert_user_is_one_of: OK';
+ END IF;
+ RAISE EXCEPTION 'User is not one of: % - cannot continue.', p_acceptable_users;
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_user_is_one_of(TEXT[]) IS 'Function that can be used to make sure that patch is being applied by one of defined users.';
+
+COMMIT;
diff --git a/src/stasis/stasis-0001.sql b/src/stasis/stasis-0001.sql
new file mode 100644
index 0000000..beb886d
--- /dev/null
+++ b/src/stasis/stasis-0001.sql
@@ -0,0 +1,194 @@
+--
+-- This file is part of Anastasis
+-- Copyright (C) 2020, 2021 Anastasis SARL SA
+--
+-- 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. If not, see <http://www.gnu.org/licenses/>
+--
+
+-- Everything in one big transaction
+BEGIN;
+
+-- Check patch versioning is in place.
+SELECT _v.register_patch('stasis-0001', NULL, NULL);
+
+
+CREATE TABLE IF NOT EXISTS anastasis_truth_payment
+ (truth_uuid BYTEA PRIMARY KEY CHECK(LENGTH(truth_uuid)=32),
+ amount_val INT8 NOT NULL,
+ amount_frac INT4 NOT NULL,
+ expiration INT8 NOT NULL);
+COMMENT ON TABLE anastasis_truth_payment
+ IS 'Records about payments for truth uploads';
+COMMENT ON COLUMN anastasis_truth_payment.truth_uuid
+ IS 'Identifier of the truth';
+COMMENT ON COLUMN anastasis_truth_payment.amount_val
+ IS 'Amount we were paid';
+COMMENT ON COLUMN anastasis_truth_payment.amount_frac
+ IS 'Amount we were paid fraction';
+COMMENT ON COLUMN anastasis_truth_payment.expiration
+ IS 'At which date will the truth payment expire';
+
+
+CREATE TABLE IF NOT EXISTS anastasis_truth
+ (truth_uuid BYTEA PRIMARY KEY CHECK(LENGTH(truth_uuid)=32),
+ key_share_data BYTEA CHECK(LENGTH(key_share_data)=80) NOT NULL,
+ method_name VARCHAR NOT NULL,
+ encrypted_truth BYTEA NOT NULL,
+ truth_mime VARCHAR NOT NULL,
+ expiration INT8 NOT NULL);
+COMMENT ON TABLE anastasis_truth
+ IS 'Truth data is needed to authenticate clients during recovery';
+COMMENT ON COLUMN anastasis_truth.truth_uuid
+ IS 'The truth UUID uniquely identifies this truth record. Not a foreign key as we may offer storing truth for free.';
+COMMENT ON COLUMN anastasis_truth.key_share_data
+ IS 'Stores the encrypted key share used to recover the key (nonce, tag and keyshare)';
+COMMENT ON COLUMN anastasis_truth.method_name
+ IS 'Defines the authentication method (SMS, E-Mail, Question..)';
+COMMENT ON COLUMN anastasis_truth.encrypted_truth
+ IS 'Stores the encrypted authentication data';
+COMMENT ON COLUMN anastasis_truth.truth_mime
+ IS 'Defines the mime type of the stored authentcation data';
+COMMENT ON COLUMN anastasis_truth.expiration
+ IS 'At which date will the truth record expire';
+
+
+CREATE TABLE IF NOT EXISTS anastasis_user
+ (user_id BYTEA PRIMARY KEY CHECK(LENGTH(user_id)=32),
+ expiration_date INT8 NOT NULL);
+COMMENT ON TABLE anastasis_user
+ IS 'Saves a user which is using Anastasis';
+COMMENT ON COLUMN anastasis_user.user_id
+ IS 'Identifier of the user account';
+COMMENT ON COLUMN anastasis_user.expiration_date
+ IS 'At which date will the user record expire';
+
+
+CREATE TABLE IF NOT EXISTS anastasis_recdoc_payment
+ (payment_id BIGSERIAL PRIMARY KEY,
+ user_id BYTEA NOT NULL REFERENCES anastasis_user(user_id),
+ post_counter INT4 NOT NULL DEFAULT 0 CHECK(post_counter >= 0),
+ amount_val INT8 NOT NULL,
+ amount_frac INT4 NOT NULL,
+ payment_identifier BYTEA NOT NULL CHECK(LENGTH(payment_identifier)=32),
+ creation_date INT8 NOT NULL,
+ paid BOOLEAN NOT NULL DEFAULT FALSE);
+COMMENT ON TABLE anastasis_recdoc_payment
+ IS 'Records a payment for a recovery document';
+COMMENT ON COLUMN anastasis_recdoc_payment.payment_id
+ IS 'Serial number which identifies the payment';
+COMMENT ON COLUMN anastasis_recdoc_payment.user_id
+ IS 'Link to the corresponding user who paid';
+COMMENT ON COLUMN anastasis_recdoc_payment.post_counter
+ IS 'For how many posts does the user pay';
+COMMENT ON COLUMN anastasis_recdoc_payment.amount_val
+ IS 'Amount we were paid';
+COMMENT ON COLUMN anastasis_recdoc_payment.amount_frac
+ IS 'Amount we were paid fraction';
+COMMENT ON COLUMN anastasis_recdoc_payment.payment_identifier
+ IS 'Payment identifier which the user has to provide';
+COMMENT ON COLUMN anastasis_recdoc_payment.creation_date
+ IS 'Creation date of the payment';
+COMMENT ON COLUMN anastasis_recdoc_payment.paid
+ IS 'Is the payment finished';
+
+
+CREATE TABLE IF NOT EXISTS anastasis_challenge_payment
+ (payment_id BIGSERIAL PRIMARY KEY,
+ truth_uuid BYTEA CHECK(LENGTH(truth_uuid)=32) NOT NULL,
+ amount_val INT8 NOT NULL,
+ amount_frac INT4 NOT NULL,
+ payment_identifier BYTEA NOT NULL CHECK(LENGTH(payment_identifier)=32),
+ creation_date INT8 NOT NULL,
+ counter INT4 NOT NULL DEFAULT 3,
+ paid BOOLEAN NOT NULL DEFAULT FALSE,
+ refunded BOOLEAN NOT NULL DEFAULT FALSE
+ );
+COMMENT ON TABLE anastasis_recdoc_payment
+ IS 'Records a payment for a challenge';
+COMMENT ON COLUMN anastasis_challenge_payment.payment_id
+ IS 'Serial number which identifies the payment';
+COMMENT ON COLUMN anastasis_challenge_payment.truth_uuid
+ IS 'Link to the corresponding challenge which is paid';
+COMMENT ON COLUMN anastasis_challenge_payment.amount_val
+ IS 'Amount we were paid';
+COMMENT ON COLUMN anastasis_challenge_payment.amount_frac
+ IS 'Amount we were paid fraction';
+COMMENT ON COLUMN anastasis_challenge_payment.payment_identifier
+ IS 'Payment identifier which the user has to provide';
+COMMENT ON COLUMN anastasis_challenge_payment.counter
+ IS 'How many more times will we issue the challenge for the given payment';
+COMMENT ON COLUMN anastasis_challenge_payment.creation_date
+ IS 'Creation date of the payment';
+COMMENT ON COLUMN anastasis_challenge_payment.paid
+ IS 'Is the payment finished';
+COMMENT ON COLUMN anastasis_challenge_payment.refunded
+ IS 'Was the payment refunded';
+
+
+CREATE TABLE IF NOT EXISTS anastasis_recoverydocument
+ (user_id BYTEA NOT NULL REFERENCES anastasis_user(user_id),
+ version INT4 NOT NULL,
+ account_sig BYTEA NOT NULL CHECK(LENGTH(account_sig)=64),
+ recovery_data_hash BYTEA NOT NULL CHECK(length(recovery_data_hash)=64),
+ recovery_data BYTEA NOT NULL,
+ PRIMARY KEY (user_id, version));
+COMMENT ON TABLE anastasis_recoverydocument
+ IS 'Stores a recovery document which contains the policy and the encrypted core secret';
+COMMENT ON COLUMN anastasis_recoverydocument.user_id
+ IS 'Link to the owner of this recovery document';
+COMMENT ON COLUMN anastasis_recoverydocument.version
+ IS 'The version of this recovery document';
+COMMENT ON COLUMN anastasis_recoverydocument.account_sig
+ IS 'Signature of the recovery document';
+COMMENT ON COLUMN anastasis_recoverydocument.recovery_data_hash
+ IS 'Hash of the recovery document to prevent unnecessary uploads';
+COMMENT ON COLUMN anastasis_recoverydocument.recovery_data
+ IS 'Contains the encrypted policy and core secret';
+
+
+CREATE TABLE IF NOT EXISTS anastasis_challengecode
+ (truth_uuid BYTEA CHECK(LENGTH(truth_uuid)=32) NOT NULL,
+ code INT8 NOT NULL,
+ creation_date INT8 NOT NULL,
+ expiration_date INT8 NOT NULL,
+ retransmission_date INT8 NOT NULL DEFAULT 0,
+ retry_counter INT4 NOT NULL);
+COMMENT ON TABLE anastasis_challengecode
+ IS 'Stores a code which is checked for the authentication by SMS, E-Mail..';
+COMMENT ON COLUMN anastasis_challengecode.truth_uuid
+ IS 'Link to the corresponding challenge which is solved';
+COMMENT ON COLUMN anastasis_challengecode.code
+ IS 'The pin code which is sent to the user and verified';
+COMMENT ON COLUMN anastasis_challengecode.creation_date
+ IS 'Creation date of the code';
+COMMENT ON COLUMN anastasis_challengecode.retransmission_date
+ IS 'When did we last transmit the challenge to the user';
+COMMENT ON COLUMN anastasis_challengecode.expiration_date
+ IS 'When will the code expire';
+COMMENT ON COLUMN anastasis_challengecode.retry_counter
+ IS 'How many tries are left for this code must be > 0';
+
+CREATE INDEX IF NOT EXISTS anastasis_challengecode_uuid_index
+ ON anastasis_challengecode
+ (truth_uuid,expiration_date);
+COMMENT ON INDEX anastasis_challengecode_uuid_index
+ IS 'for challenge lookup';
+
+CREATE INDEX IF NOT EXISTS anastasis_challengecode_expiration_index
+ ON anastasis_challengecode
+ (truth_uuid,expiration_date);
+COMMENT ON INDEX anastasis_challengecode_expiration_index
+ IS 'for challenge garbage collection';
+
+
+-- Complete transaction
+COMMIT;
diff --git a/src/stasis/stasis-postgres.conf b/src/stasis/stasis-postgres.conf
new file mode 100644
index 0000000..0d9b209
--- /dev/null
+++ b/src/stasis/stasis-postgres.conf
@@ -0,0 +1,6 @@
+[stasis-postgres]
+CONFIG = "postgres:///anastasis"
+
+# Where are the SQL files to setup our tables?
+# Important: this MUST end with a "/"!
+SQL_DIR = $DATADIR/sql/
diff --git a/src/stasis/test_anastasis_db.c b/src/stasis/test_anastasis_db.c
new file mode 100644
index 0000000..5e21530
--- /dev/null
+++ b/src/stasis/test_anastasis_db.c
@@ -0,0 +1,344 @@
+/*
+ This file is part of Anastasis
+ Copyright (C) 2020, 2021 Taler Systems SA
+
+ Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file anastasis/test_anastasis_db.c
+ * @brief testcase for anastasis postgres db plugin
+ * @author Marcello Stanisci
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "anastasis_database_lib.h"
+#include "anastasis_util_lib.h"
+#include <gnunet/gnunet_signatures.h>
+
+
+#define FAILIF(cond) \
+ do { \
+ if (! (cond)) { break;} \
+ GNUNET_break (0); \
+ goto drop; \
+ } while (0)
+
+#define RND_BLK(ptr) \
+ GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, ptr, sizeof (*ptr))
+
+/**
+ * Global return value for the test. Initially -1, set to 0 upon
+ * completion. Other values indicate some kind of error.
+ */
+static int result;
+
+/**
+ * Handle to the plugin we are testing.
+ */
+static struct ANASTASIS_DatabasePlugin *plugin;
+
+
+/**
+ * Main function that will be run by the scheduler.
+ *
+ * @param cls closure with config
+ */
+static void
+run (void *cls)
+{
+ struct GNUNET_CONFIGURATION_Handle *cfg = cls;
+ struct TALER_Amount amount;
+ struct ANASTASIS_PaymentSecretP paymentSecretP;
+ struct ANASTASIS_CRYPTO_AccountPublicKeyP accountPubP;
+ struct ANASTASIS_AccountSignatureP accountSig;
+ struct ANASTASIS_AccountSignatureP res_account_sig;
+ struct GNUNET_HashCode recoveryDataHash;
+ struct GNUNET_HashCode res_recovery_data_hash;
+ struct GNUNET_HashCode r;
+ struct GNUNET_TIME_Relative rel_time;
+ struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid;
+ struct ANASTASIS_CRYPTO_EncryptedKeyShareP key_share;
+ unsigned int post_counter;
+ char *mime_type;
+ char *method;
+ uint32_t docVersion;
+ uint32_t res_version;
+ size_t recoverydatasize;
+ void *res_recovery_data = NULL;
+ struct ANASTASIS_CRYPTO_EncryptedKeyShareP res_key_share;
+ bool paid;
+ bool valid_counter;
+ uint32_t recversion = 1;
+ unsigned char aes_gcm_tag[16];
+ const char *recovery_data = "RECOVERY_DATA";
+ uint64_t challenge_code = 1234;
+ struct GNUNET_HashCode c_hash;
+ struct ANASTASIS_UploadSignaturePS usp = {
+ .purpose.purpose = htonl (GNUNET_SIGNATURE_PURPOSE_TEST),
+ .purpose.size = htonl (sizeof (usp))
+ };
+
+ if (NULL == (plugin = ANASTASIS_DB_plugin_load (cfg)))
+ {
+ result = 77;
+ return;
+ }
+ if (GNUNET_OK != plugin->drop_tables (plugin->cls))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Dropping tables failed\n");
+ result = 77;
+ return;
+ }
+ ANASTASIS_DB_plugin_unload (plugin);
+ if (NULL == (plugin = ANASTASIS_DB_plugin_load (cfg)))
+ {
+ result = 77;
+ return;
+ }
+
+ GNUNET_CRYPTO_hash (recovery_data,
+ strlen (recovery_data),
+ &recoveryDataHash);
+ RND_BLK (&paymentSecretP);
+ RND_BLK (&aes_gcm_tag);
+ post_counter = 2;
+ mime_type = "Picture";
+ method = "Method";
+ TALER_string_to_amount ("EUR:30",&amount);
+
+ GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
+ &truth_uuid,
+ sizeof (truth_uuid));
+ rel_time = GNUNET_TIME_UNIT_MONTHS;
+
+ GNUNET_assert (GNUNET_OK ==
+ TALER_string_to_amount ("EUR:1",
+ &amount));
+
+ memset (&key_share, 1, sizeof (key_share));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->store_truth (plugin->cls,
+ &truth_uuid,
+ &key_share,
+ mime_type,
+ "encrypted_truth",
+ strlen ("encrypted_truth"),
+ method,
+ rel_time));
+
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
+ plugin->check_payment_identifier (plugin->cls,
+ &paymentSecretP,
+ &paid,
+ &valid_counter));
+
+ memset (&accountPubP, 2, sizeof (accountPubP));
+ memset (&accountSig, 3, sizeof (accountSig));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->record_recdoc_payment (plugin->cls,
+ &accountPubP,
+ post_counter,
+ &paymentSecretP,
+ &amount));
+ {
+ struct GNUNET_TIME_Absolute res_time;
+
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->increment_lifetime (plugin->cls,
+ &accountPubP,
+ &paymentSecretP,
+ rel_time,
+ &res_time));
+ }
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->check_payment_identifier (plugin->cls,
+ &paymentSecretP,
+ &paid,
+ &valid_counter));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
+ plugin->check_challenge_payment (plugin->cls,
+ &paymentSecretP,
+ &truth_uuid,
+ &paid));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->record_challenge_payment (plugin->cls,
+ &truth_uuid,
+ &paymentSecretP,
+ &amount));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->update_challenge_payment (plugin->cls,
+ &truth_uuid,
+ &paymentSecretP));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->check_challenge_payment (plugin->cls,
+ &paymentSecretP,
+ &truth_uuid,
+ &paid));
+ FAILIF (! paid);
+ FAILIF (ANASTASIS_DB_STORE_STATUS_SUCCESS !=
+ plugin->store_recovery_document (plugin->cls,
+ &accountPubP,
+ &accountSig,
+ &recoveryDataHash,
+ recovery_data,
+ strlen (recovery_data),
+ &paymentSecretP,
+ &docVersion));
+ {
+ uint32_t vrs;
+ struct GNUNET_TIME_Absolute exp;
+
+ FAILIF (ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED !=
+ plugin->lookup_account (plugin->cls,
+ &accountPubP,
+ &exp,
+ &r,
+ &vrs));
+ }
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->get_key_share (plugin->cls,
+ &truth_uuid,
+ &res_key_share));
+ FAILIF (0 !=
+ GNUNET_memcmp (&res_key_share,
+ &key_share));
+
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->get_recovery_document (plugin->cls,
+ &accountPubP,
+ recversion,
+ &res_account_sig,
+ &res_recovery_data_hash,
+ &recoverydatasize,
+ &res_recovery_data));
+ FAILIF (0 != memcmp (res_recovery_data,
+ recovery_data,
+ strlen (recovery_data)));
+ GNUNET_free (res_recovery_data);
+
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->get_latest_recovery_document (plugin->cls,
+ &accountPubP,
+ &res_account_sig,
+ &res_recovery_data_hash,
+ &recoverydatasize,
+ &res_recovery_data,
+ &res_version));
+ FAILIF (0 != memcmp (res_recovery_data,
+ recovery_data,
+ strlen (recovery_data)));
+ GNUNET_free (res_recovery_data);
+
+ {
+ struct GNUNET_TIME_Absolute rt;
+
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->create_challenge_code (plugin->cls,
+ &truth_uuid,
+ GNUNET_TIME_UNIT_HOURS,
+ GNUNET_TIME_UNIT_DAYS,
+ 3, /* retry counter */
+ &rt,
+ &challenge_code));
+ FAILIF (0 != rt.abs_value_us);
+ }
+ {
+ struct GNUNET_TIME_Absolute rt;
+ uint64_t c2;
+
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ plugin->create_challenge_code (plugin->cls,
+ &truth_uuid,
+ GNUNET_TIME_UNIT_HOURS,
+ GNUNET_TIME_UNIT_DAYS,
+ 3, /* retry counter */
+ &rt,
+ &c2));
+ FAILIF (c2 != challenge_code);
+ }
+ ANASTASIS_hash_answer (123,
+ &c_hash);
+ FAILIF (ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH !=
+ plugin->verify_challenge_code (plugin->cls,
+ &truth_uuid,
+ &c_hash));
+
+ ANASTASIS_hash_answer (challenge_code,
+ &c_hash);
+ FAILIF (ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED !=
+ plugin->verify_challenge_code (plugin->cls,
+ &truth_uuid,
+ &c_hash));
+
+ if (-1 == result)
+ result = 0;
+
+drop:
+ GNUNET_break (GNUNET_OK ==
+ plugin->drop_tables (plugin->cls));
+ ANASTASIS_DB_plugin_unload (plugin);
+ if (NULL != plugin)
+ {
+ plugin = NULL;
+ }
+}
+
+
+int
+main (int argc,
+ char *const argv[])
+{
+ const char *plugin_name;
+ char *config_filename;
+ char *testname;
+ struct GNUNET_CONFIGURATION_Handle *cfg;
+
+ result = -1;
+ if (NULL == (plugin_name = strrchr (argv[0], (int) '-')))
+ {
+ GNUNET_break (0);
+ return -1;
+ }
+ /* FIRST get the libtalerutil initialization out
+ of the way. Then throw that one away, and force
+ the SYNC defaults to be used! */
+ (void) TALER_project_data_default ();
+ GNUNET_OS_init (ANASTASIS_project_data_default ());
+ GNUNET_log_setup (argv[0], "DEBUG", NULL);
+ plugin_name++;
+ GNUNET_asprintf (&testname,
+ "%s",
+ plugin_name);
+ GNUNET_asprintf (&config_filename,
+ "test_anastasis_db_%s.conf",
+ testname);
+ cfg = GNUNET_CONFIGURATION_create ();
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_load (cfg,
+ config_filename))
+ {
+ GNUNET_break (0);
+ GNUNET_free (config_filename);
+ GNUNET_free (testname);
+ return 2;
+ }
+ GNUNET_SCHEDULER_run (&run,
+ cfg);
+ GNUNET_CONFIGURATION_destroy (cfg);
+ GNUNET_free (config_filename);
+ GNUNET_free (testname);
+ return result;
+}
+
+
+/* end of test_anastasis_db.c */
diff --git a/src/stasis/test_anastasis_db_postgres.conf b/src/stasis/test_anastasis_db_postgres.conf
new file mode 100644
index 0000000..8971a62
--- /dev/null
+++ b/src/stasis/test_anastasis_db_postgres.conf
@@ -0,0 +1,10 @@
+[anastasis]
+#The DB plugin to use
+DB = postgres
+
+[taler]
+CURRENCY = EUR
+
+[stasis-postgres]
+#The connection string the plugin has to use for connecting to the database
+CONFIG = postgres:///anastasischeck