diff options
Diffstat (limited to 'src/stasis')
-rw-r--r-- | src/stasis/Datenbank-Schema.xml | 1 | ||||
-rw-r--r-- | src/stasis/Makefile.am | 95 | ||||
-rw-r--r-- | src/stasis/anastasis-dbinit.c | 112 | ||||
-rw-r--r-- | src/stasis/anastasis_db_plugin.c | 146 | ||||
-rw-r--r-- | src/stasis/anastasis_db_postgres.conf | 7 | ||||
-rw-r--r-- | src/stasis/drop0001.sql | 37 | ||||
-rw-r--r-- | src/stasis/plugin_anastasis_postgres.c | 2301 | ||||
-rw-r--r-- | src/stasis/stasis-0000.sql | 293 | ||||
-rw-r--r-- | src/stasis/stasis-0001.sql | 194 | ||||
-rw-r--r-- | src/stasis/stasis-postgres.conf | 6 | ||||
-rw-r--r-- | src/stasis/test_anastasis_db.c | 344 | ||||
-rw-r--r-- | src/stasis/test_anastasis_db_postgres.conf | 10 |
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 |