From ff964e4d21c7f577dbcc0fd296f4569366dad82e Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Tue, 16 Aug 2022 16:16:59 +0100 Subject: [PATCH 01/12] Add argon2 binding for neko --- lib/argon2/.gitignore | 6 +++ lib/argon2/CMakeLists.txt | 46 ++++++++++++++++++++ lib/argon2/README.md | 12 ++++++ lib/argon2/haxelib.json | 7 ++++ lib/argon2/native/argon2.c | 70 +++++++++++++++++++++++++++++++ lib/argon2/src/argon2/Argon2id.hx | 21 ++++++++++ lib/argon2/test.hxml | 4 ++ lib/argon2/test/Test.hx | 13 ++++++ libs.hxml | 1 + 9 files changed, 180 insertions(+) create mode 100644 lib/argon2/.gitignore create mode 100644 lib/argon2/CMakeLists.txt create mode 100644 lib/argon2/README.md create mode 100644 lib/argon2/haxelib.json create mode 100644 lib/argon2/native/argon2.c create mode 100644 lib/argon2/src/argon2/Argon2id.hx create mode 100644 lib/argon2/test.hxml create mode 100644 lib/argon2/test/Test.hx diff --git a/lib/argon2/.gitignore b/lib/argon2/.gitignore new file mode 100644 index 000000000..cd2d3d017 --- /dev/null +++ b/lib/argon2/.gitignore @@ -0,0 +1,6 @@ +*.n +*.ndll + +obj/ +bin/ +build/ diff --git a/lib/argon2/CMakeLists.txt b/lib/argon2/CMakeLists.txt new file mode 100644 index 000000000..cc2aebb3f --- /dev/null +++ b/lib/argon2/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.16.3) + +project(Argon2Ndll C) + +include(ExternalProject) + +set(ARGON2_LIB ${CMAKE_BINARY_DIR}/libs/src/Argon2/libargon2.a) +set(ARGON2_INCLUDE ${CMAKE_BINARY_DIR}/libs/src/Argon2/include) + +ExternalProject_Add(Argon2 + PREFIX ${CMAKE_BINARY_DIR}/libs + DOWNLOAD_DIR ${CMAKE_BINARY_DIR}/libs/download + URL https://github.com/P-H-C/phc-winner-argon2/archive/refs/tags/20190702.tar.gz + URL_HASH SHA256=daf972a89577f8772602bf2eb38b6a3dd3d922bf5724d45e7f9589b5e830442c + BUILD_COMMAND cd ${CMAKE_BINARY_DIR}/libs/src/Argon2 && CFLAGS=-fPIC make + CONFIGURE_COMMAND echo skip config + INSTALL_COMMAND echo skip install + BYPRODUCTS ${ARGON2_LIB} +) + +add_custom_command( + OUTPUT ${ARGON2_INCLUDE}/argon2.h + DEPENDS Argon2 +) + +add_custom_command( + OUTPUT ${ARGON2_LIB} + DEPENDS Argon2 +) + +add_library(argon2.ndll MODULE native/argon2.c) + +target_include_directories(argon2.ndll PRIVATE ${ARGON2_INCLUDE}) +target_link_libraries(argon2.ndll libneko.so ${ARGON2_LIB}) + +set_target_properties(argon2.ndll + PROPERTIES + PREFIX "" + OUTPUT_NAME argon2 + SUFFIX .ndll +) + +install( + TARGETS argon2.ndll + DESTINATION ${CMAKE_SOURCE_DIR}/ndll/Linux64 +) diff --git a/lib/argon2/README.md b/lib/argon2/README.md new file mode 100644 index 000000000..c9786d027 --- /dev/null +++ b/lib/argon2/README.md @@ -0,0 +1,12 @@ +# Argon2 Neko bindings + +## Building + +To build the ndll file, run the following commands: + +```sh +mkdir build +cd build +cmake .. +make +``` diff --git a/lib/argon2/haxelib.json b/lib/argon2/haxelib.json new file mode 100644 index 000000000..c55dff798 --- /dev/null +++ b/lib/argon2/haxelib.json @@ -0,0 +1,7 @@ +{ + "name": "argon2", + "description": "Neko wrapper for C argon2 implementation", + "version": "0.0.0", + "classPath": "src", + "license": "MIT" +} diff --git a/lib/argon2/native/argon2.c b/lib/argon2/native/argon2.c new file mode 100644 index 000000000..d3861fbc0 --- /dev/null +++ b/lib/argon2/native/argon2.c @@ -0,0 +1,70 @@ +#include +#include + +#include "argon2.h" + +#define HASHLEN 32 + +static void handle_error(int rc) { + buffer b = alloc_buffer("Argon2 Error: "); + buffer_append(b, argon2_error_message(rc)); + buffer_append(b, "\n"); + val_throw(buffer_to_string(b)); +} + +value generate_argon2id_raw_hash(value time_cost, value memory_cost, value parallelism, value password, value salt) { + printf("hello\n"); + val_check(time_cost, int); + val_check(memory_cost, int); + val_check(parallelism, int); + val_check(password, string); + val_check(salt, string); + + value hash = alloc_empty_string(HASHLEN); + + int rc = argon2id_hash_raw(val_int(time_cost), val_int(memory_cost), val_int(parallelism), val_string(password), val_strlen(password), val_string(salt), val_strlen(salt), val_string(hash), HASHLEN); + if (rc != ARGON2_OK) { + handle_error(rc); + } + + return hash; +} + +value generate_argon2id_hash(value time_cost, value memory_cost, value parallelism, value password, value salt) { + val_check(time_cost, int); + val_check(memory_cost, int); + val_check(parallelism, int); + val_check(password, string); + val_check(salt, string); + + size_t salt_len = val_strlen(salt); + size_t password_len = val_strlen(password); + size_t encoded_len = argon2_encodedlen(val_int(time_cost), val_int(memory_cost), val_int(parallelism), salt_len, HASHLEN, Argon2_id); + + // encoded_len takes into account null terminator, however, alloc_empty_string adds an extra byte for that anyway + value hash_string = alloc_empty_string(encoded_len - 1); + + int rc = argon2id_hash_encoded(val_int(time_cost), val_int(memory_cost), val_int(parallelism), val_string(password), password_len, val_string(salt), salt_len, HASHLEN, val_string(hash_string), encoded_len); + if (rc != ARGON2_OK) { + handle_error(rc); + } + + return hash_string; +} + +value verify_argon2id(value hash, value password) { + val_check(hash, string); + val_check(password, string); + + int rc = argon2id_verify(val_string(hash), val_string(password), val_strlen(password)); + if (rc == ARGON2_OK) + return val_true; + if (rc == ARGON2_VERIFY_MISMATCH) + return val_false; + handle_error(rc); + return val_false; +} + +DEFINE_PRIM(generate_argon2id_raw_hash, 5); +DEFINE_PRIM(generate_argon2id_hash, 5); +DEFINE_PRIM(verify_argon2id, 2); diff --git a/lib/argon2/src/argon2/Argon2id.hx b/lib/argon2/src/argon2/Argon2id.hx new file mode 100644 index 000000000..285d1322f --- /dev/null +++ b/lib/argon2/src/argon2/Argon2id.hx @@ -0,0 +1,21 @@ +package argon2; + +import haxe.io.Bytes; + +class Argon2id { + public static function generateHash(password:String, salt:Bytes, timeCost:Int, memoryCost:Int, parallelism:Int):String { + return new String(untyped generate_argon2id_hash(timeCost, memoryCost, parallelism, password.__s, salt.getData())); + } + + public static function generateRawHash(password:String, salt:Bytes, timeCost:Int, memoryCost:Int, parallelism:Int) { + return new String(untyped generate_argon2id_raw_hash(timeCost, memoryCost, parallelism, password.__s, salt.getData())); + } + + public static function verify(hash:String, password:String) { + return untyped verify_argon2id(hash.__s, password.__s); + } + + static var generate_argon2id_hash = neko.Lib.load("argon2", "generate_argon2id_hash", 5); + static var generate_argon2id_raw_hash = neko.Lib.load("argon2", "generate_argon2id_raw_hash", 5); + static var verify_argon2id = neko.Lib.load("argon2", "verify_argon2id", 2); +} diff --git a/lib/argon2/test.hxml b/lib/argon2/test.hxml new file mode 100644 index 000000000..92800d361 --- /dev/null +++ b/lib/argon2/test.hxml @@ -0,0 +1,4 @@ +--main Test +-p test +-lib argon2 +--neko bin/test.n diff --git a/lib/argon2/test/Test.hx b/lib/argon2/test/Test.hx new file mode 100644 index 000000000..385bae7c0 --- /dev/null +++ b/lib/argon2/test/Test.hx @@ -0,0 +1,13 @@ +import argon2.Argon2id; +import haxe.io.Bytes; + +function main() { + final hash = Argon2id.generateRawHash("hello", Bytes.ofString("tesfdfdafdafsagfagahraegfaharegh"), 2, 1 << 16, 1); + trace(hash); + + final hash = Argon2id.generateHash("hello", Bytes.ofString("tesfdfdafdafsagfagahraegfaharegh"), 2, 1 << 16, 1); + trace(hash); + + trace(Argon2id.verify(hash, "hello")); + trace(Argon2id.verify(hash, "hi")); +} diff --git a/libs.hxml b/libs.hxml index ef9ca5b34..d93b20062 100644 --- a/libs.hxml +++ b/libs.hxml @@ -30,3 +30,4 @@ -cmd curl -sSLk https://lib.haxe.org/files/3.0/utest-1,9,6.zip -o haxelib_global/utest.zip && neko run.n install --always --skip-dependencies haxelib_global/utest.zip -cmd curl -sSLk https://lib.haxe.org/files/3.0/hxnodejs-12,1,0.zip -o haxelib_global/hxnodejs.zip && neko run.n install --always --skip-dependencies haxelib_global/hxnodejs.zip -cmd neko run.n dev record-macros lib/record-macros +-cmd neko run.n dev argon2 lib/argon2 From 0299b05a788a1ee3fa27c9cd9abe788f93e9fbe2 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Mon, 22 Aug 2022 23:13:06 +0100 Subject: [PATCH 02/12] Implement argon2id hashing --- server_api.hxml | 15 ++++++++------- server_each.hxml | 3 ++- skeema/User.sql | 1 + src/haxelib/server/Hashing.hx | 28 ++++++++++++++++++++++++++++ src/haxelib/server/SiteDb.hx | 1 + 5 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 src/haxelib/server/Hashing.hx diff --git a/server_api.hxml b/server_api.hxml index bcaedbe8c..2459209e7 100644 --- a/server_api.hxml +++ b/server_api.hxml @@ -1,7 +1,8 @@ --cp src --neko www/api/3.0/index.n --main haxelib.server.Repo --lib aws-sdk-neko --lib record-macros --dce no --D haxelib_api +-cp src +-neko www/api/3.0/index.n +-main haxelib.server.Repo +-lib aws-sdk-neko +-lib record-macros +-lib argon2 +-dce no +-D haxelib-api diff --git a/server_each.hxml b/server_each.hxml index aac315465..d83028bb6 100644 --- a/server_each.hxml +++ b/server_each.hxml @@ -11,7 +11,8 @@ -lib aws-sdk-neko -lib record-macros -lib html-haxe-code-highlighter:0.1.2 +-lib argon2 -D server -D getter_support # https://github.com/HaxeFoundation/haxe/issues/4903 ---macro keep('StringBuf') \ No newline at end of file +--macro keep('StringBuf') diff --git a/skeema/User.sql b/skeema/User.sql index 3a90d1b93..345820fba 100644 --- a/skeema/User.sql +++ b/skeema/User.sql @@ -4,5 +4,6 @@ CREATE TABLE `User` ( `fullname` mediumtext NOT NULL, `email` mediumtext NOT NULL, `pass` mediumtext NOT NULL, + `salt` binary(32) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/src/haxelib/server/Hashing.hx b/src/haxelib/server/Hashing.hx new file mode 100644 index 000000000..652ad0986 --- /dev/null +++ b/src/haxelib/server/Hashing.hx @@ -0,0 +1,28 @@ +package haxelib.server; + +class Hashing { + /** + Generates a cryptographically secure random salt. + **/ + public static function generateSalt():haxe.io.Bytes { + // currently only works on Linux + var randomFile = sys.io.File.read("/dev/urandom"); + var salt = randomFile.read(32); + randomFile.close(); + return salt; + } + + /** + Hashes `password` using `salt` + **/ + public static inline function hash(password:String, salt:haxe.io.Bytes) { + return argon2.Argon2id.generateHash(password, salt, 2, 1 << 16, 1); + } + + /** + Verifies whether `password` matches `hash` after being hashed. + **/ + public static inline function verify(hash:String, password:String):Bool { + return argon2.Argon2id.verify(hash, password); + } +} diff --git a/src/haxelib/server/SiteDb.hx b/src/haxelib/server/SiteDb.hx index 482a0e71a..416667c6e 100644 --- a/src/haxelib/server/SiteDb.hx +++ b/src/haxelib/server/SiteDb.hx @@ -32,6 +32,7 @@ class User extends Object { public var fullname : String; public var email : String; public var pass : String; + public var salt : haxe.io.Bytes; } From dbd6b09c60bd9a1d3c0572f75bd80b8ae69d0b39 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Mon, 22 Aug 2022 23:52:20 +0100 Subject: [PATCH 03/12] [earthly] Update to build argon2.ndll --- .earthlyignore | 4 ++++ Earthfile | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 .earthlyignore diff --git a/.earthlyignore b/.earthlyignore new file mode 100644 index 000000000..6c844307c --- /dev/null +++ b/.earthlyignore @@ -0,0 +1,4 @@ +lib/argon2/build +lib/argon2/bin +lib/argon2/ndll +lib/argon2/obj diff --git a/Earthfile b/Earthfile index 33eaa6950..4a900166c 100644 --- a/Earthfile +++ b/Earthfile @@ -32,6 +32,18 @@ neko: RUN tar --strip-components=1 -xf "$FILENAME" -C neko SAVE ARTIFACT neko/* +INSTALL_NEKO: + COMMAND + ARG NEKOPATH=/neko + COPY +neko/* "$NEKOPATH/" + ARG PREFIX=/usr/local + RUN bash -c "ln -s \"$NEKOPATH\"/{neko,nekoc,nekoml,nekotools} \"$PREFIX/bin/\"" + RUN bash -c "ln -s \"$NEKOPATH\"/libneko.* \"$PREFIX/lib/\"" + RUN bash -c "ln -s \"$NEKOPATH\"/neko.h \"$PREFIX/include/\"" + RUN mkdir -p "$PREFIX/lib/neko/" + RUN bash -c "ln -s \"$NEKOPATH\"/*.ndll \"$PREFIX/lib/neko/\"" + RUN ldconfig + haxe: ARG FILENAME=haxe.tar.gz RUN curl -fsSL "https://github.com/HaxeFoundation/haxe/releases/download/4.3.4/haxe-4.3.4-linux64.tar.gz" -o "$FILENAME" @@ -101,10 +113,7 @@ devcontainer-base: && rm -rf /var/lib/apt/lists/* # install neko - COPY +neko/neko /usr/bin/neko - COPY +neko/libneko.so* /usr/lib/ - RUN mkdir -p /usr/lib/neko/ - COPY +neko/*.ndll /usr/lib/neko/ + DO +INSTALL_NEKO RUN neko -version # install haxe @@ -223,6 +232,7 @@ haxelib-deps: USER $USERNAME COPY --chown=$USER_UID:$USER_GID libs.hxml run.n . COPY --chown=$USER_UID:$USER_GID lib/record-macros lib/record-macros + COPY --chown=$USER_UID:$USER_GID lib/argon2 lib/argon2 RUN mkdir -p haxelib_global RUN neko run.n setup haxelib_global RUN haxe libs.hxml && rm haxelib_global/*.zip @@ -334,11 +344,30 @@ aws-ndll: FROM +haxelib-deps SAVE ARTIFACT /workspace/haxelib_global/aws-sdk-neko/*/ndll/Linux64/aws.ndll +argon2-ndll: + # install build-essential, cmake, and neko + RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + build-essential \ + cmake \ + && rm -r /var/lib/apt/lists/* + + DO +INSTALL_NEKO + RUN neko -version + + COPY lib/argon2 lib/argon2 + RUN mkdir lib/argon2/build + RUN cmake -B lib/argon2/build -S lib/argon2 + RUN make -C lib/argon2/build Argon2 + RUN make -C lib/argon2/build + SAVE ARTIFACT lib/argon2/build/argon2.ndll + haxelib-server-builder: FROM haxe:3.4 WORKDIR /workspace COPY lib/record-macros lib/record-macros + COPY lib/argon2 lib/argon2 COPY --chown=$USER_UID:$USER_GID +node-modules-dev/node_modules node_modules COPY --chown=$USER_UID:$USER_GID +dts2hx-externs/dts2hx-generated lib/dts2hx-generated COPY --chown=$USER_UID:$USER_GID +haxelib-deps/haxelib_global haxelib_global @@ -443,6 +472,7 @@ haxelib-server: && apachectl stop COPY +aws-ndll/aws.ndll /usr/lib/x86_64-linux-gnu/neko/aws.ndll + COPY +argon2-ndll/argon2.ndll /usr/lib/x86_64-linux-gnu/neko/argon2.ndll # Need rclone to do the upload to R2 COPY +rclone/rclone /usr/local/bin/ From 88ffe1e9851b773933c40c1cfb381013a1397a2e Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Wed, 21 Sep 2022 17:59:58 +0100 Subject: [PATCH 04/12] Implement database updating system This allows password to be automatically rehashed if the database is old --- skeema/Meta.sql | 5 ++++ src/haxelib/server/SiteDb.hx | 8 +++++- src/haxelib/server/Update.hx | 47 ++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 skeema/Meta.sql create mode 100644 src/haxelib/server/Update.hx diff --git a/skeema/Meta.sql b/skeema/Meta.sql new file mode 100644 index 000000000..1c5a1a5c5 --- /dev/null +++ b/skeema/Meta.sql @@ -0,0 +1,5 @@ +CREATE TABLE `Meta` ( + `id` enum('') NOT NULL DEFAULT '', + `dbVersion` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/src/haxelib/server/SiteDb.hx b/src/haxelib/server/SiteDb.hx index 416667c6e..c83f0c8e8 100644 --- a/src/haxelib/server/SiteDb.hx +++ b/src/haxelib/server/SiteDb.hx @@ -118,6 +118,11 @@ class Developer extends Object { } +class Meta extends Object { + var id:SString<1>; + public var dbVersion:SUInt; +} + class SiteDb { static var db : Connection; //TODO: this whole configuration business is rather messy to say the least @@ -146,7 +151,8 @@ class SiteDb { Project.manager, Tag.manager, Version.manager, - Developer.manager + Developer.manager, + Meta.manager ]; for (m in managers) if (!TableCreate.exists(m)) diff --git a/src/haxelib/server/Update.hx b/src/haxelib/server/Update.hx new file mode 100644 index 000000000..d8a7d9518 --- /dev/null +++ b/src/haxelib/server/Update.hx @@ -0,0 +1,47 @@ +package haxelib.server; + +import sys.db.TableCreate; +import haxelib.server.SiteDb; + +/** + Handles server database updates from old versions of the database. +**/ +class Update { + /** The current version of the database. **/ + static inline var CURRENT_VERSION = 1; + + /** + Checks which updates are needed and if there are any needed, runs them. + **/ + public static function runNeededUpdates() { + var meta = Meta.manager.all().first(); + + if (meta == null) { + // no meta data stored yet, so create it + meta = new Meta(); + meta.dbVersion = 0; + meta.insert(); + } + + if (meta.dbVersion == 0) { + rehashPasswords(); + } + + meta.dbVersion = CURRENT_VERSION; + meta.update(); + } + + static function rehashPasswords() { + // script used to update password hashes from md5 to md5 rehashed with argon2id + var users = User.manager.all(); + + for (user in users) { + var md5Hash = user.pass; + + user.salt = Hashing.generateSalt(); + user.pass = Hashing.hash(md5Hash, user.salt); + user.update(); + } + } + +} From 071d207fcaebc211a69541b428d1d94131d1023c Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Wed, 21 Sep 2022 18:00:23 +0100 Subject: [PATCH 05/12] [test] Add tests for database updating system --- Earthfile | 2 + integration_tests.hxml | 6 +- test/IntegrationTests.hx | 1 + .../integration/TestServerDatabaseUpdate.hx | 111 ++++++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 test/tests/integration/TestServerDatabaseUpdate.hx diff --git a/Earthfile b/Earthfile index 4a900166c..6f2cd53e1 100644 --- a/Earthfile +++ b/Earthfile @@ -540,6 +540,8 @@ ci-tests: COPY hx3compat hx3compat COPY lib/node-sys-db lib/node-sys-db COPY lib/record-macros lib/record-macros + COPY lib/argon2 lib/argon2 + COPY +argon2-ndll/argon2.ndll argon2.ndll COPY src src COPY www www COPY test test diff --git a/integration_tests.hxml b/integration_tests.hxml index 37ab3b6ef..364917a79 100644 --- a/integration_tests.hxml +++ b/integration_tests.hxml @@ -1,7 +1,9 @@ -cp src -cp test --lib hx3compat -cp hx4compat/std -cp crypto/src +-lib hx3compat +-lib record-macros +-lib argon2 -main IntegrationTests --neko bin/integration_tests.n \ No newline at end of file +-neko bin/integration_tests.n diff --git a/test/IntegrationTests.hx b/test/IntegrationTests.hx index 532c1466e..69987a21c 100644 --- a/test/IntegrationTests.hx +++ b/test/IntegrationTests.hx @@ -250,6 +250,7 @@ class IntegrationTests extends TestBase { runner.add(new tests.integration.TestHg()); runner.add(new tests.integration.TestMisc()); runner.add(new tests.integration.TestFixRepo()); + runner.add(new tests.integration.TestServerDatabaseUpdate()); final success = runner.run(); diff --git a/test/tests/integration/TestServerDatabaseUpdate.hx b/test/tests/integration/TestServerDatabaseUpdate.hx new file mode 100644 index 000000000..51b35dd5e --- /dev/null +++ b/test/tests/integration/TestServerDatabaseUpdate.hx @@ -0,0 +1,111 @@ +package tests.integration; + +import haxe.crypto.Md5; +import haxelib.server.Update; +import haxelib.server.Hashing; +import haxelib.server.SiteDb; + +class TestServerDatabaseUpdate extends IntegrationTests { + + override function setup() { + super.setup(); + SiteDb.init(); + } + + override function tearDown() { + SiteDb.cleanup(); + super.tearDown(); + } + + /** + Simulates an old database still containing md5 hashes + **/ + function simulateOldDatabase(users:Array<{ + user:String, + email:String, + fullname:String, + pw:String + }>) { + final meta = Meta.manager.all().first(); + meta.dbVersion = 0; + meta.update(); + + for (data in users) { + final user = new User(); + user.name = data.user; + user.fullname = data.fullname; + user.email = data.email; + user.pass = Md5.encode(data.pw); + // ignore salt + user.insert(); + } + } + + function testUpdate() { + simulateOldDatabase([foo, bar]); + + Update.runNeededUpdates(); + + final fooAccount = User.manager.search($name == foo.user).first(); + + assertEquals(fooAccount.pass, Hashing.hash(Md5.encode(foo.pw), fooAccount.salt)); + assertTrue(Hashing.verify(fooAccount.pass, Md5.encode(foo.pw))); + assertEquals(fooAccount.salt.length, 32); + + final barAccount = User.manager.search($name == bar.user).first(); + + assertEquals(barAccount.pass, Hashing.hash(Md5.encode(bar.pw), barAccount.salt)); + assertTrue(Hashing.verify(barAccount.pass, Md5.encode(bar.pw))); + assertEquals(barAccount.salt.length, 32); + } + + function createNewUserAccount(data:{ + user:String, + email:String, + fullname:String, + pw:String + }) { + final user = new User(); + user.name = data.user; + user.fullname = data.fullname; + user.email = data.email; + final salt = Hashing.generateSalt(); + user.pass = Hashing.hash(Md5.encode(data.pw), salt); + user.salt = salt; + user.insert(); + } + + function testReUpdate() { + simulateOldDatabase([foo]); + + // should fix foo account + Update.runNeededUpdates(); + + createNewUserAccount(bar); + createNewUserAccount(deepAuthor); + + // re-update should not change anything + Update.runNeededUpdates(); + + final fooAccount = User.manager.search($name == foo.user).first(); + + assertEquals(fooAccount.pass, Hashing.hash(Md5.encode(foo.pw), fooAccount.salt)); + assertTrue(Hashing.verify(fooAccount.pass, Md5.encode(foo.pw))); + assertEquals(fooAccount.salt.length, 32); + + // accounts added after first update + + final barAccount = User.manager.search($name == bar.user).first(); + + assertEquals(barAccount.pass, Hashing.hash(Md5.encode(bar.pw), barAccount.salt)); + assertTrue(Hashing.verify(barAccount.pass, Md5.encode(bar.pw))); + assertEquals(barAccount.salt.length, 32); + + final deepAccount = User.manager.search($name == deepAuthor.user).first(); + + assertEquals(deepAccount.pass, Hashing.hash(Md5.encode(deepAuthor.pw), deepAccount.salt)); + assertTrue(Hashing.verify(deepAccount.pass, Md5.encode(deepAuthor.pw))); + assertEquals(deepAccount.salt.length, 32); + } + +} From 8661cad58db0097009dd0890509c0e49edf83a41 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Tue, 4 Oct 2022 17:55:13 +0100 Subject: [PATCH 06/12] Run database update script in SiteDb init for now --- src/haxelib/server/SiteDb.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/haxelib/server/SiteDb.hx b/src/haxelib/server/SiteDb.hx index c83f0c8e8..6ca3a9da3 100644 --- a/src/haxelib/server/SiteDb.hx +++ b/src/haxelib/server/SiteDb.hx @@ -157,6 +157,8 @@ class SiteDb { for (m in managers) if (!TableCreate.exists(m)) TableCreate.create(m); + + Update.runNeededUpdates(); } static public function cleanup() { From 7b98118e1a1685d50e9d8b7fa37bad9213de1516 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Mon, 22 Aug 2022 23:13:06 +0100 Subject: [PATCH 07/12] Update server code to use argon2id - Passwords are now hashed on the server, with a salt. --- src/haxelib/server/Repo.hx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/haxelib/server/Repo.hx b/src/haxelib/server/Repo.hx index 6d8fb7a22..cdd6cee10 100644 --- a/src/haxelib/server/Repo.hx +++ b/src/haxelib/server/Repo.hx @@ -111,6 +111,11 @@ class Repo implements SiteApi { }; } + static inline function setPassword(user:User, password:String) { + user.salt = Hashing.generateSalt(); + user.pass = Hashing.hash(password, user.salt); + } + public function register( name : String, pass : String, mail : String, fullname : String ) : Void { if( name.length < 3 ) throw "User name must be at least 3 characters"; @@ -125,9 +130,9 @@ class Repo implements SiteApi { var u = new User(); u.name = name; - u.pass = pass; u.email = mail; u.fullname = fullname; + setPassword(u,pass); u.insert(); } @@ -145,9 +150,13 @@ class Repo implements SiteApi { throw "User '"+user+"' is not a developer of project '"+prj+"'"; } + static inline function verifyPassword(user:User, password:String):Bool { + return Hashing.verify(user.pass, password); + } + public function checkPassword( user : String, pass : String ) : Bool { var u = User.manager.search({ name : user }).first(); - return u != null && u.pass == pass; + return u != null && verifyPassword(u,pass); } public function getSubmitId() : String { @@ -201,7 +210,7 @@ class Repo implements SiteApi { Manager.cnx.startTransaction(); var u = User.manager.search({ name : user }).first(); - if( u == null || u.pass != pass ) { + if( u == null || !verifyPassword(u,pass) ) { Manager.cnx.rollback(); throw "Invalid username or password"; } From 2c8ed1e42d8b4668ac0b87cdad80a9e1eda72cbd Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Wed, 5 Oct 2022 22:11:18 +0100 Subject: [PATCH 08/12] Add test environment for skeema --- skeema/.skeema | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skeema/.skeema b/skeema/.skeema index 079e97052..2a30bb2f4 100644 --- a/skeema/.skeema +++ b/skeema/.skeema @@ -7,3 +7,9 @@ flavor=mysql:5.7 host=haxe-org.ct0xwjh6v08k.eu-west-1.rds.amazonaws.com port=3306 schema=haxelib + +[test] +flavor=mysql:5.7 +host=localhost +port=3306 +schema=haxelib From cf55aa1320f7d8e3f751786018b57249a0b91730 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Sat, 26 Nov 2022 19:24:53 +0000 Subject: [PATCH 09/12] Automatically add new table columns This will be done via sql statements for now, because I'm not sure how to run `skeema push` automatically. --- src/haxelib/server/Update.hx | 7 ++++++- test/tests/integration/TestServerDatabaseUpdate.hx | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/haxelib/server/Update.hx b/src/haxelib/server/Update.hx index d8a7d9518..0dc881ac2 100644 --- a/src/haxelib/server/Update.hx +++ b/src/haxelib/server/Update.hx @@ -1,6 +1,5 @@ package haxelib.server; -import sys.db.TableCreate; import haxelib.server.SiteDb; /** @@ -32,6 +31,12 @@ class Update { } static function rehashPasswords() { + // add missing columns first + sys.db.Manager.cnx.request(" + ALTER TABLE User + ADD COLUMN salt binary(32) NOT NULL; + "); + // script used to update password hashes from md5 to md5 rehashed with argon2id var users = User.manager.all(); diff --git a/test/tests/integration/TestServerDatabaseUpdate.hx b/test/tests/integration/TestServerDatabaseUpdate.hx index 51b35dd5e..27e0054e0 100644 --- a/test/tests/integration/TestServerDatabaseUpdate.hx +++ b/test/tests/integration/TestServerDatabaseUpdate.hx @@ -39,6 +39,10 @@ class TestServerDatabaseUpdate extends IntegrationTests { // ignore salt user.insert(); } + sys.db.Manager.cnx.request(" + ALTER TABLE User + DROP COLUMN salt; + "); } function testUpdate() { From 1742fd62641f70a718649266960be544ba413f33 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Sat, 26 Nov 2022 19:26:23 +0000 Subject: [PATCH 10/12] Do not run updates on fresh databases --- src/haxelib/server/SiteDb.hx | 17 ++++++++++++++--- src/haxelib/server/Update.hx | 9 +++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/haxelib/server/SiteDb.hx b/src/haxelib/server/SiteDb.hx index 6ca3a9da3..90de951c2 100644 --- a/src/haxelib/server/SiteDb.hx +++ b/src/haxelib/server/SiteDb.hx @@ -154,11 +154,22 @@ class SiteDb { Developer.manager, Meta.manager ]; - for (m in managers) - if (!TableCreate.exists(m)) + + var hasOldTables = false; + + for (m in managers) { + if (TableCreate.exists(m)) { + hasOldTables = true; + } else { TableCreate.create(m); + } + } - Update.runNeededUpdates(); + if (hasOldTables) { + Update.runNeededUpdates(); + } else { + Update.setupFresh(); + } } static public function cleanup() { diff --git a/src/haxelib/server/Update.hx b/src/haxelib/server/Update.hx index 0dc881ac2..23509bdbd 100644 --- a/src/haxelib/server/Update.hx +++ b/src/haxelib/server/Update.hx @@ -30,6 +30,15 @@ class Update { meta.update(); } + /** + Sets up a fresh database + **/ + public static function setupFresh() { + var meta = new Meta(); + meta.dbVersion = CURRENT_VERSION; + meta.insert(); + } + static function rehashPasswords() { // add missing columns first sys.db.Manager.cnx.request(" From 3bba6c33f7e174ea31d9917c4a47689bed8d4a3b Mon Sep 17 00:00:00 2001 From: Tobi Laskowski Date: Fri, 12 Apr 2024 17:22:25 +0100 Subject: [PATCH 11/12] Update record macros to fix haxe 4.3 error in tests --- lib/record-macros | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/record-macros b/lib/record-macros index aa7703f18..00afa3924 160000 --- a/lib/record-macros +++ b/lib/record-macros @@ -1 +1 @@ -Subproject commit aa7703f18cf5ec5af584e237fb333b4d567c478a +Subproject commit 00afa3924e8478203dd500f8b8a6f2174b2f8e13 From dd2da1f57d0c55f69b6fb2553fa4a8fc44b0c418 Mon Sep 17 00:00:00 2001 From: Tobiasz Laskowski Date: Tue, 19 Nov 2024 19:41:50 +0000 Subject: [PATCH 12/12] Use transactions for hashing update methods --- src/haxelib/server/Update.hx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/haxelib/server/Update.hx b/src/haxelib/server/Update.hx index 23509bdbd..92fe245a0 100644 --- a/src/haxelib/server/Update.hx +++ b/src/haxelib/server/Update.hx @@ -13,6 +13,8 @@ class Update { Checks which updates are needed and if there are any needed, runs them. **/ public static function runNeededUpdates() { + sys.db.Manager.cnx.startTransaction(); + var meta = Meta.manager.all().first(); if (meta == null) { @@ -28,15 +30,21 @@ class Update { meta.dbVersion = CURRENT_VERSION; meta.update(); + + sys.db.Manager.cnx.commit(); } /** Sets up a fresh database **/ public static function setupFresh() { + sys.db.Manager.cnx.startTransaction(); + var meta = new Meta(); meta.dbVersion = CURRENT_VERSION; meta.insert(); + + sys.db.Manager.cnx.commit(); } static function rehashPasswords() {