diff --git a/auth.lua b/auth.lua deleted file mode 100644 index ef20623..0000000 --- a/auth.lua +++ /dev/null @@ -1,46 +0,0 @@ --- 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) - - 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 - -minetest.register_on_prejoinplayer(function(name) - print("minetest.register_on_prejoinplayer(" .. name .. ")") - -end) \ No newline at end of file diff --git a/init.lua b/init.lua index c0b8c8d..01ddefd 100644 --- a/init.lua +++ b/init.lua @@ -13,7 +13,7 @@ otp = { dofile(MP.."/functions.lua") dofile(MP.."/onboard.lua") -dofile(MP.."/auth.lua") +dofile(MP.."/join.lua") dofile(MP.."/privs.lua") if minetest.get_modpath("mtt") and mtt.enabled then diff --git a/join.lua b/join.lua new file mode 100644 index 0000000..9fe59f5 --- /dev/null +++ b/join.lua @@ -0,0 +1,87 @@ +local FORMNAME = "otp-check" + +-- time for otp code verification +local otp_time = 300 + +-- playername => start_time +local otp_sessions = {} + +-- privs to revoke until the verification code is validated +local temp_revoke_privs = {"interact", "shout", "privs", "basic_privs", "server", "ban", "kick"} + +local function revoke_privs(playername) + local privs = minetest.get_player_privs(playername) + if otp.storage:get_string(playername .. "_privs") == "" then + otp.storage:set_string(playername .. "_privs", minetest.serialize(privs)) + for _, priv in ipairs(temp_revoke_privs) do + privs[priv] = nil + minetest.set_player_privs(playername, privs) + end + end +end + +local function regrant_privs(playername) + local stored_priv_str = otp.storage:get_string(playername .. "_privs") + if stored_priv_str ~= "" then + local privs = minetest.deserialize(stored_priv_str) + minetest.set_player_privs(playername, privs) + otp.storage:set_string(playername .. "_privs", "") + end +end + +-- Code formspec on join for otp enabled players +minetest.register_on_joinplayer(function(player) + local playername = player:get_player_name() + if minetest.check_player_privs(playername, "otp_enabled") then + -- start otp session time + otp_sessions[player:get_player_name()] = os.time() + + -- revoke important privs and re-grant again on code-verification + revoke_privs(playername) + + -- send verification formspec + local formspec = "size[10,2]" .. + "label[1,0;Please enter your OTP code below]" .. + "field[1,1.3;4,1;code;Code;]" .. + "button_exit[5,1;3,1;submit;Verify]" + + minetest.show_formspec(playername, FORMNAME, formspec) + end +end) + +-- clear otp session on leave +minetest.register_on_leaveplayer(function(player) + local playername = player:get_player_name() + otp_sessions[playername] = nil +end) + +-- check sessions periodically and kick if timed out +local function session_check() + local now = os.time() + for name, start_time in pairs(otp_sessions) do + if (now - start_time) > otp_time then + minetest.kick_player(name, "OTP Code validation timed out") + otp_sessions[name] = nil + end + end + minetest.after(5, session_check) +end +minetest.after(5, session_check) + +-- otp check +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + 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 + minetest.chat_send_player(playername, "OTP Code validation succeeded") + otp_sessions[playername] = nil + regrant_privs(playername) + else + minetest.kick_player(playername, "OTP Code validation failed") + end +end) \ No newline at end of file diff --git a/onboard.lua b/onboard.lua index 9d223f4..e1b2a7c 100644 --- a/onboard.lua +++ b/onboard.lua @@ -1,22 +1,20 @@ -local FORMNAME = "otp-enable" +local FORMNAME = "otp-onboard" minetest.register_chatcommand("otp_disable", { - privs = { otp_enabled = true }, + description = "Disable the otp verification", + privs = { otp_enabled = true, interact = true }, func = function(name) -- clear priv local privs = minetest.get_player_privs(name) - privs.otp_enabled = true + privs.otp_enabled = nil minetest.set_player_privs(name, privs) return true, "OTP login disabled" end }) minetest.register_chatcommand("otp_enable", { + description = "Enable the otp verification", 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 @@ -38,9 +36,11 @@ minetest.register_chatcommand("otp_enable", { end local png = otp.create_qr_png(code) - local formspec = "size[10,10]" .. - "image[1,0.6;5,5;^[png:" .. minetest.encode_base64(png) .. "]" .. - "field[1,9;5,1;code;Code;]" + local formspec = "size[9,10]" .. + "image[1.5,0.6;7,7;^[png:" .. minetest.encode_base64(png) .. "]" .. + "label[1,7;Use the above QR code in your OTP-App to obtain a verification code]" .. + "field[1,9;4,1;code;Code;]" .. + "button_exit[5,8.7;3,1;submit;Verify]" minetest.show_formspec(name, FORMNAME, formspec) end diff --git a/privs.lua b/privs.lua index c73330a..b8d7656 100644 --- a/privs.lua +++ b/privs.lua @@ -1,4 +1,5 @@ minetest.register_privilege("otp_enabled", { - description = "otp enabled player" + description = "otp enabled player", + give_to_singleplayer = false }) \ No newline at end of file diff --git a/readme.md b/readme.md index 602e177..d911bd9 100644 --- a/readme.md +++ b/readme.md @@ -1,11 +1,45 @@ # (T)OTP mod for minetest -* State: **WIP** +* State: **Stable** +# Overview + +Lets security-aware players use the `/otp_enable` command to protect their account with a second factor. + +Players that have the OTP enabled have to enter a verification code upon joining the game. + +# OTP Authenticator app + +* https://freeotp.github.io/ + +# Screenshots + +OTP verification form +![](./screenshot1.png) + +OTP Setup form +![](./screenshot2.png) + +# Links / References + +* https://en.wikipedia.org/wiki/Time-based_one-time_password +* https://en.wikipedia.org/wiki/HMAC-based_one-time_password +* https://en.wikipedia.org/wiki/HMAC +* https://github.com/google/google-authenticator/wiki/Key-Uri-Format + +# Chatcommands + +* `/otp_enable` Starts the OTP onboarding process +* `/otp_disable` Disables the OTP Login + +# Privileges + +* `otp_enabled` Players with this privilege have to verify the OTP Code upon login (automatically granted on successful `/otp_enable`) # License * Code: `MIT` +* Textures: `CC-BY-SA 3.0` * "basexx.lua" `MIT` https://github.com/aiq/basexx/blob/master/lib/basexx.lua * "qrencode.lua" `BSD` https://github.com/speedata/luaqrcode/blob/master/qrencode.lua \ No newline at end of file diff --git a/screenshot1.png b/screenshot1.png new file mode 100644 index 0000000..88d58ee Binary files /dev/null and b/screenshot1.png differ diff --git a/screenshot2.png b/screenshot2.png new file mode 100644 index 0000000..7cff496 Binary files /dev/null and b/screenshot2.png differ