// Manual live-database verification of the SA-16 rehash path (not part // of the meson test suite): boots a real SQLite store through Inversion // + DatabaseConfigurator exactly as SpryModule does, registers a user, // force-stales their stored hash by writing one at INTERACTIVE limits, // then authenticates and asserts the stored hash was transparently // rewritten at current MODERATE parameters — same password, epoch NOT // bumped, login succeeding throughout. // // Build & run (from the Spry checkout, against a built libspry): // valac tests/RehashProbe.vala vapi/libsodium.vapi -o /tmp/rehash-probe \ // --vapidir vapi --vapidir /src \ // --pkg spry-0.2 --pkg statum-0.1 --pkg astralis-0.1 \ // --pkg inversion-0.1 --pkg invercargill-1 --pkg invercargill-json \ // --pkg invercargill-sql --pkg invercargill-sql-inversion \ // --pkg sqlite3 --pkg libxml-2.0 --pkg json-glib-1.0 \ // --pkg glib-2.0 --pkg gobject-2.0 --pkg gio-2.0 \ // -X -lsodium -X -L/src -X -lspry-0.2 -X -Wl,-rpath,/src // /tmp/rehash-probe /tmp/rehash-probe.sqlite using Inversion; using InvercargillSqlInversion; using Invercargill.Expressions; using Invercargill.DataStructures; using InvercargillSql.Orm; using Spry; using Spry.Authentication; int failures = 0; void check(bool condition, string label) { if (condition) { print("PASS %s\n", label); } else { failures++; print("FAIL %s\n", label); } } async int probe(string db_path) { GLib.FileUtils.remove(db_path); var container = new Container(); try { var db = container.configure_with(); db.register_connection(@"sqlite://$(db_path)"); db.add_migration(); db.add_migration(); db.add_entity(Authentication.UserEntity.entity_mapping); db.add_entity(Authentication.UserPermissionEntity.entity_mapping); db.add_projection(Authentication.UserProjection.projection_mapping); container.register_scoped(); db.migrate_on_startup(); container.initialise(); } catch (Error e) { printerr("probe wiring threw: %s\n", e.message); return 1; } string password = "live rehash probe password"; try { var scope = container.create_scope(); var users = scope.resolve(); var orm = scope.resolve(); var registered = yield users.register_user("probe-user", "probe@example.com", "Probe", "User", new GLib.DateTime.utc(1990, 1, 1, 0, 0, 0.0), password, true); int64 id = registered.id; check(!UserService.hash_needs_rehash(load_hash(orm, id)), "registration stores current parameters"); // Force-stale the stored hash (INTERACTIVE limits), exactly as // an old deployment's row looks after a parameter bump. string stale = Sodium.PasswordHashing.hash(password, Sodium.PasswordHashing.OPSLIMIT_INTERACTIVE, Sodium.PasswordHashing.MEMLIMIT_INTERACTIVE); store_hash(orm, id, stale); check(UserService.hash_needs_rehash(load_hash(orm, id)), "stale parameters are recognised as needing a rehash"); var epoch_before = load_epoch(orm, id); var logged_in = yield users.authenticate_user("probe-user", password); var epoch_after = load_epoch(orm, id); string upgraded = load_hash(orm, id); check(logged_in != null, "login succeeds against a stale hash"); check(!UserService.hash_needs_rehash(upgraded), "the stored hash was rewritten at current parameters on login"); check(Sodium.PasswordHashing.check(upgraded, password), "the same password verifies against the rewritten hash"); check(epoch_after.compare(epoch_before) == 0, "the session-validity epoch was untouched by the rehash"); check((yield users.authenticate_user("probe-user", "wrong password entirely")) == null, "a wrong password is rejected against the upgraded hash"); } catch (Error e) { printerr("probe threw: %s\n", e.message); return 1; } print("---- live rehash probe: %d failures ----\n", failures); return failures > 0 ? 1 : 0; } int main(string[] args) { string db_path = args.length > 1 ? args[1] : "/tmp/rehash-probe.sqlite"; var loop = new GLib.MainLoop(); int result = 1; probe.begin(db_path, (o, r) => { result = probe.end(r); loop.quit(); }); loop.run(); return result; } private UserEntity load_user(OrmSession orm, int64 user_id) throws Error { var user = orm.query() .where(expr("id == $0", elem(user_id))) .first(); if (user == null) { throw new GLib.IOError.FAILED(@"no user row for id $(user_id)"); } return (!)user; } private string load_hash(OrmSession orm, int64 user_id) throws Error { return load_user(orm, user_id).password_hash; } private GLib.DateTime load_epoch(OrmSession orm, int64 user_id) throws Error { return load_user(orm, user_id).sessions_valid_after; } private void store_hash(OrmSession orm, int64 user_id, string hash) throws Error { var user = load_user(orm, user_id); user.password_hash = hash; orm.update(user); }