RehashProbe.vala 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. // Manual live-database verification of the SA-16 rehash path (not part
  2. // of the meson test suite): boots a real SQLite store through Inversion
  3. // + DatabaseConfigurator exactly as SpryModule does, registers a user,
  4. // force-stales their stored hash by writing one at INTERACTIVE limits,
  5. // then authenticates and asserts the stored hash was transparently
  6. // rewritten at current MODERATE parameters — same password, epoch NOT
  7. // bumped, login succeeding throughout.
  8. //
  9. // Build & run (from the Spry checkout, against a built libspry):
  10. // valac tests/RehashProbe.vala vapi/libsodium.vapi -o /tmp/rehash-probe \
  11. // --vapidir vapi --vapidir <builddir>/src \
  12. // --pkg spry-0.2 --pkg statum-0.1 --pkg astralis-0.1 \
  13. // --pkg inversion-0.1 --pkg invercargill-1 --pkg invercargill-json \
  14. // --pkg invercargill-sql --pkg invercargill-sql-inversion \
  15. // --pkg sqlite3 --pkg libxml-2.0 --pkg json-glib-1.0 \
  16. // --pkg glib-2.0 --pkg gobject-2.0 --pkg gio-2.0 \
  17. // -X -lsodium -X -L<builddir>/src -X -lspry-0.2 -X -Wl,-rpath,<builddir>/src
  18. // /tmp/rehash-probe /tmp/rehash-probe.sqlite
  19. using Inversion;
  20. using InvercargillSqlInversion;
  21. using Invercargill.Expressions;
  22. using Invercargill.DataStructures;
  23. using InvercargillSql.Orm;
  24. using Spry;
  25. using Spry.Authentication;
  26. int failures = 0;
  27. void check(bool condition, string label) {
  28. if (condition) {
  29. print("PASS %s\n", label);
  30. } else {
  31. failures++;
  32. print("FAIL %s\n", label);
  33. }
  34. }
  35. async int probe(string db_path) {
  36. GLib.FileUtils.remove(db_path);
  37. var container = new Container();
  38. try {
  39. var db = container.configure_with<DatabaseConfigurator>();
  40. db.register_connection(@"sqlite://$(db_path)");
  41. db.add_migration<Authentication.Migrations.M0001_Initial>();
  42. db.add_migration<Authentication.Migrations.M0002_SessionsValidAfter>();
  43. db.add_entity<Authentication.UserEntity>(Authentication.UserEntity.entity_mapping);
  44. db.add_entity<Authentication.UserPermissionEntity>(Authentication.UserPermissionEntity.entity_mapping);
  45. db.add_projection<Authentication.UserProjection>(Authentication.UserProjection.projection_mapping);
  46. container.register_scoped<Authentication.UserService>();
  47. db.migrate_on_startup();
  48. container.initialise();
  49. } catch (Error e) {
  50. printerr("probe wiring threw: %s\n", e.message);
  51. return 1;
  52. }
  53. string password = "live rehash probe password";
  54. try {
  55. var scope = container.create_scope();
  56. var users = scope.resolve<UserService>();
  57. var orm = scope.resolve<OrmSession>();
  58. var registered = yield users.register_user("probe-user", "probe@example.com",
  59. "Probe", "User", new GLib.DateTime.utc(1990, 1, 1, 0, 0, 0.0), password, true);
  60. int64 id = registered.id;
  61. check(!UserService.hash_needs_rehash(load_hash(orm, id)),
  62. "registration stores current parameters");
  63. // Force-stale the stored hash (INTERACTIVE limits), exactly as
  64. // an old deployment's row looks after a parameter bump.
  65. string stale = Sodium.PasswordHashing.hash(password,
  66. Sodium.PasswordHashing.OPSLIMIT_INTERACTIVE,
  67. Sodium.PasswordHashing.MEMLIMIT_INTERACTIVE);
  68. store_hash(orm, id, stale);
  69. check(UserService.hash_needs_rehash(load_hash(orm, id)),
  70. "stale parameters are recognised as needing a rehash");
  71. var epoch_before = load_epoch(orm, id);
  72. var logged_in = yield users.authenticate_user("probe-user", password);
  73. var epoch_after = load_epoch(orm, id);
  74. string upgraded = load_hash(orm, id);
  75. check(logged_in != null, "login succeeds against a stale hash");
  76. check(!UserService.hash_needs_rehash(upgraded),
  77. "the stored hash was rewritten at current parameters on login");
  78. check(Sodium.PasswordHashing.check(upgraded, password),
  79. "the same password verifies against the rewritten hash");
  80. check(epoch_after.compare(epoch_before) == 0,
  81. "the session-validity epoch was untouched by the rehash");
  82. check((yield users.authenticate_user("probe-user", "wrong password entirely")) == null,
  83. "a wrong password is rejected against the upgraded hash");
  84. } catch (Error e) {
  85. printerr("probe threw: %s\n", e.message);
  86. return 1;
  87. }
  88. print("---- live rehash probe: %d failures ----\n", failures);
  89. return failures > 0 ? 1 : 0;
  90. }
  91. int main(string[] args) {
  92. string db_path = args.length > 1 ? args[1] : "/tmp/rehash-probe.sqlite";
  93. var loop = new GLib.MainLoop();
  94. int result = 1;
  95. probe.begin(db_path, (o, r) => {
  96. result = probe.end(r);
  97. loop.quit();
  98. });
  99. loop.run();
  100. return result;
  101. }
  102. private UserEntity load_user(OrmSession orm, int64 user_id) throws Error {
  103. var user = orm.query<UserEntity>()
  104. .where(expr("id == $0", elem<int64?>(user_id)))
  105. .first();
  106. if (user == null) {
  107. throw new GLib.IOError.FAILED(@"no user row for id $(user_id)");
  108. }
  109. return (!)user;
  110. }
  111. private string load_hash(OrmSession orm, int64 user_id) throws Error {
  112. return load_user(orm, user_id).password_hash;
  113. }
  114. private GLib.DateTime load_epoch(OrmSession orm, int64 user_id) throws Error {
  115. return load_user(orm, user_id).sessions_valid_after;
  116. }
  117. private void store_hash(OrmSession orm, int64 user_id, string hash) throws Error {
  118. var user = load_user(orm, user_id);
  119. user.password_hash = hash;
  120. orm.update<UserEntity>(user);
  121. }