diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a53fe9..2a0cc88 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,4 +17,4 @@ jobs: fetch-depth: 0 submodules: recursive - name: test - run: docker-compose up --exit-code-from test + run: docker-compose up --exit-code-from test test diff --git a/auth.lua b/auth.lua index eac1a74..ef20623 100644 --- a/auth.lua +++ b/auth.lua @@ -1,15 +1,46 @@ --- TODO - +-- builtin auth handler local auth_handler = minetest.get_auth_handler() - local old_get_auth = auth_handler.get_auth +-- time for otp to be enabled until properly logged in +local otp_time = 300 + +-- playername => start_time +local otp_sessions = {} + +minetest.register_on_joinplayer(function(player) + -- reset otp session upon login + local playername = player:get_player_name() + otp_sessions[player:get_player_name()] = nil + print("minetest.register_on_joinplayer(" .. playername .. ")") +end) + +-- override "get_auth" from builtin auth handler auth_handler.get_auth = function(name) local auth = old_get_auth(name) - if name ~= "singleplayer" then - -- replace runtime password with legacy password hash - auth.password = minetest.get_password_hash(name, "enter") + print("auth_handler.get_auth(" .. name .. ")") + if name == "singleplayer" or not auth.privileges.otp_enabled then + -- singleplayer or otp not set up + return auth end + + -- minetest.disconnect_player(name, "something, something") + + local now = os.time() + local otp_session = otp_sessions[name] + if not otp_session or (now - otp_session) > otp_time then + -- otp session expired or not set up + otp_sessions[name] = now + end + + -- replace runtime password with legacy password hash + --auth.password = minetest.get_password_hash(name, "enter") + return auth -end \ No newline at end of file +end + +minetest.register_on_prejoinplayer(function(name) + print("minetest.register_on_prejoinplayer(" .. name .. ")") + +end) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index aedc543..8813ac1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,8 +8,15 @@ services: - "./:/root/.minetest/worlds/world/worldmods/otp/" - "./test/minetest.conf:/minetest.conf" - "world:/root/.minetest/worlds/world" + + minetest: + image: registry.gitlab.com/minetest/minetest/server:5.6.1 + user: root ports: - "30000:30000/udp" + volumes: + - "./:/root/.minetest/worlds/world/worldmods/otp/" + - "world:/root/.minetest/worlds/world" volumes: world: {} \ No newline at end of file diff --git a/functions.lua b/functions.lua index b3dcc57..7f7ea15 100644 --- a/functions.lua +++ b/functions.lua @@ -117,7 +117,8 @@ local function left_pad(str, s, len) return str end -function otp.generate_totp(key, unix_time) +function otp.generate_totp(secret_b32, unix_time) + local key = otp.basexx.from_base32(secret_b32) unix_time = unix_time or os.time() local tx = 30 @@ -167,4 +168,14 @@ function otp.generate_secret() s = s .. string.char(string.byte(buf, i)) end return s +end + +-- get or generate per-player secret b32 ecoded +function otp.get_player_secret_b32(name) + local secret_b32 = otp.storage:get_string(name .. "_secret") + if secret_b32 == "" then + secret_b32 = otp.basexx.to_base32(otp.generate_secret()) + otp.storage:set_string(name .. "_secret", secret_b32) + end + return secret_b32 end \ No newline at end of file diff --git a/functions.spec.lua b/functions.spec.lua index 64893ce..7819fcb 100644 --- a/functions.spec.lua +++ b/functions.spec.lua @@ -30,14 +30,13 @@ end) mtt.register("otp.generate_totp", function(callback) local expected_code = 699847 local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7" - local secret = otp.basexx.from_base32(secret_b32) local unix_time = 1640995200 - local code, valid_seconds = otp.generate_totp(secret, unix_time) + local code, valid_seconds = otp.generate_totp(secret_b32, unix_time) assert(code == ""..expected_code) assert(valid_seconds > 0) - code, valid_seconds = otp.generate_totp(secret) + code, valid_seconds = otp.generate_totp(secret_b32) print("Current code: " .. code .. " valid for " .. valid_seconds .. " seconds") callback() end) diff --git a/init.lua b/init.lua index d7cc321..c0b8c8d 100644 --- a/init.lua +++ b/init.lua @@ -14,6 +14,7 @@ otp = { dofile(MP.."/functions.lua") dofile(MP.."/onboard.lua") dofile(MP.."/auth.lua") +dofile(MP.."/privs.lua") if minetest.get_modpath("mtt") and mtt.enabled then dofile(MP.."/functions.spec.lua") diff --git a/onboard.lua b/onboard.lua index 318f7f2..9d223f4 100644 --- a/onboard.lua +++ b/onboard.lua @@ -1,10 +1,23 @@ local FORMNAME = "otp-enable" -local secret = otp.generate_secret() -local secret_b32 = otp.basexx.to_base32(secret) +minetest.register_chatcommand("otp_disable", { + privs = { otp_enabled = true }, + func = function(name) + -- clear priv + local privs = minetest.get_player_privs(name) + privs.otp_enabled = true + minetest.set_player_privs(name, privs) + return true, "OTP login disabled" + end +}) minetest.register_chatcommand("otp_enable", { func = function(name) + if name == "singleplayer" then + return false, "OTP not available in singleplayer" + end + + -- issuer name local issuer = "Minetest" if minetest.settings:get("server_name") ~= "" then issuer = minetest.settings:get("server_name") @@ -12,12 +25,17 @@ minetest.register_chatcommand("otp_enable", { issuer = minetest.settings:get("server_address") end + local secret_b32 = otp.get_player_secret_b32(name) + + -- url for the qr code local url = "otpauth://totp/" .. issuer .. ":" .. name .. "?algorithm=SHA1&" .. "digits=6&issuer=" .. issuer .. "&period=30&" .. "secret=" .. secret_b32 local ok, code = otp.qrcode(url) - assert(ok) + if not ok then + return false, "qr code generation failed" + end local png = otp.create_qr_png(code) local formspec = "size[10,10]" .. @@ -34,12 +52,18 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) end if fields.code then - print("Validating code for " .. player:get_player_name()) - local expected_code = otp.generate_totp(secret) + local playername = player:get_player_name() + local secret_b32 = otp.get_player_secret_b32(playername) + local expected_code = otp.generate_totp(secret_b32) if expected_code == fields.code then - print("Valid") + -- set priv + local privs = minetest.get_player_privs(playername) + privs.otp_enabled = true + minetest.set_player_privs(playername, privs) + + minetest.chat_send_player(playername, "Code validation succeeded, OTP login enabled") else - print("Invalid") + minetest.chat_send_player(playername, "Code validation failed!") end end diff --git a/privs.lua b/privs.lua new file mode 100644 index 0000000..c73330a --- /dev/null +++ b/privs.lua @@ -0,0 +1,4 @@ + +minetest.register_privilege("otp_enabled", { + description = "otp enabled player" +}) \ No newline at end of file