fork creation done

This commit is contained in:
localhost_frssoft 2023-09-27 10:57:15 +00:00
parent 376d163d08
commit 3466db227f
24 changed files with 416 additions and 1652 deletions

View file

@ -1,12 +0,0 @@
on: [push, pull_request]
name: luacheck
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: lint
uses: Roang-zero1/factorio-mod-luacheck@master
with:
luacheckrc_url: ""

View file

@ -1,20 +0,0 @@
name: test
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
ENGINE_VERSION: [5.3.0, 5.4.0, 5.5.0, 5.6.1, latest]
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: recursive
- name: test
run: docker-compose up --exit-code-from test test

View file

@ -1,5 +1,5 @@
globals = { globals = {
"otp", "fediauth",
"minetest" "minetest"
} }
@ -13,10 +13,5 @@ read_globals = {
"dump", "dump2", "dump", "dump2",
"VoxelArea", "VoxelArea",
-- testing
"mtt"
} }
files["qrencode.lua"] = {
ignore = {"631"}
}

View file

@ -1,22 +0,0 @@
version: "3.6"
services:
test:
build: ./test
user: root
volumes:
- "./:/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: {}

View file

@ -51,7 +51,7 @@ local function lshift(x, by)
end end
-- big-endian uint64 of a number -- big-endian uint64 of a number
function otp.write_uint64_be(v) function fediauth.write_uint64_be(v)
local b1 = bitand( rshift(v, 56), 0xFF ) local b1 = bitand( rshift(v, 56), 0xFF )
local b2 = bitand( rshift(v, 48), 0xFF ) local b2 = bitand( rshift(v, 48), 0xFF )
local b3 = bitand( rshift(v, 40), 0xFF ) local b3 = bitand( rshift(v, 40), 0xFF )
@ -73,7 +73,7 @@ for _=1,64 do
end end
-- hmac generation -- hmac generation
function otp.hmac(key, message) function fediauth.hmac(key, message)
local i_key_pad = "" local i_key_pad = ""
for i=1,64 do for i=1,64 do
i_key_pad = i_key_pad .. string.char(bitxor(string.byte(key, i) or 0x00, string.byte(i_pad, i))) i_key_pad = i_key_pad .. string.char(bitxor(string.byte(key, i) or 0x00, string.byte(i_pad, i)))
@ -117,16 +117,16 @@ local function left_pad(str, s, len)
return str return str
end end
function otp.generate_totp(secret_b32, unix_time) function fediauth.generate_tfediauth(secret_b32, unix_time)
local key = otp.basexx.from_base32(secret_b32) local key = fediauth.basexx.from_base32(secret_b32)
unix_time = unix_time or os.time() unix_time = unix_time or os.time()
local tx = 30 local tx = 30
local ct = math.floor(unix_time / tx) local ct = math.floor(unix_time / tx)
local counter = otp.write_uint64_be(ct) local counter = fediauth.write_uint64_be(ct)
local valid_seconds = ((ct * tx) + tx) - unix_time local valid_seconds = ((ct * tx) + tx) - unix_time
local hmac = otp.hmac(key, counter) local hmac = fediauth.hmac(key, counter)
-- https://www.rfc-editor.org/rfc/rfc4226#section-5.4 -- https://www.rfc-editor.org/rfc/rfc4226#section-5.4
local offset = bitand(string.byte(hmac, #hmac), 0xF) local offset = bitand(string.byte(hmac, #hmac), 0xF)
@ -141,7 +141,7 @@ function otp.generate_totp(secret_b32, unix_time)
return padded_code, valid_seconds return padded_code, valid_seconds
end end
function otp.create_qr_png(data) function fediauth.create_qr_png(data)
local height = #data + 2 local height = #data + 2
local width = height local width = height
@ -174,7 +174,7 @@ function otp.create_qr_png(data)
return minetest.encode_png(width, height, png_data, 2) return minetest.encode_png(width, height, png_data, 2)
end end
function otp.generate_secret() function fediauth.generate_secret()
local buf = minetest.sha1("" .. math.random(10000), true) local buf = minetest.sha1("" .. math.random(10000), true)
local s = "" local s = ""
for i=1,20 do for i=1,20 do
@ -184,30 +184,46 @@ function otp.generate_secret()
end end
-- get or generate per-player secret b32 ecoded -- get or generate per-player secret b32 ecoded
function otp.get_player_secret_b32(name) function fediauth.get_player_secret_b32(name)
local secret_b32 = otp.storage:get_string(name .. "_secret") local secret_b32 = fediauth.storage:get_string(name .. "_secret")
if secret_b32 == "" then if secret_b32 == "" then
secret_b32 = otp.basexx.to_base32(otp.generate_secret()) secret_b32 = fediauth.basexx.to_base32(fediauth.generate_secret())
otp.storage:set_string(name .. "_secret", secret_b32) fediauth.storage:set_string(name .. "_secret", secret_b32)
end end
return secret_b32 return secret_b32
end end
-- returns true if the player is otp enabled _and_ set up properly -- returns true if the player is fediauth enabled _and_ set up properly
function otp.is_player_enabled(name) function fediauth.is_player_enabled(name)
local has_secret = otp.storage:get_string(name .. "_secret") ~= "" local has_secret = fediauth.storage:get_string(name .. "_secret") ~= ""
local has_priv = minetest.check_player_privs(name, "otp_enabled") local has_priv = minetest.check_player_privs(name, "fediauth_enabled")
return has_secret and has_priv return has_secret and has_priv
end end
function otp.check_code(secret_b32, code, time) function fediauth.is_player_bypassed(name)
local has_priv = minetest.check_player_privs(name, "fediauth_bypass")
return has_priv
end
function fediauth.check_code(secret_b32, code, time)
time = time or os.time() time = time or os.time()
for _, t_offset in ipairs({0, -30, 30}) do for _, t_offset in ipairs({0, -30, 30, -60, 60}) do
local expected_code = otp.generate_totp(secret_b32, time + t_offset) local expected_code = fediauth.generate_tfediauth(secret_b32, time + t_offset)
if expected_code == code then if expected_code == code then
return true return true
end end
end end
return false return false
end end
function fediauth.give_code(secret_b32, time)
time = time or os.time()
local codeseq = {}
for _, t_offset in ipairs({60, 30, 0, -30, -60}) do
local expected_code = fediauth.generate_tfediauth(secret_b32, time + t_offset)
table.insert(codeseq, expected_code)
end
return codeseq
end

View file

@ -1,80 +0,0 @@
mtt.register("otp.hmac", function(callback)
local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7"
local secret = otp.basexx.from_base32(secret_b32)
local unix_time = 1640995200
local expected_hmac = otp.basexx.from_base64("m04YheEb7i+ThPMUJEfVXVybZFo=")
assert(#expected_hmac == 20)
local tx = 30
local ct = math.floor(unix_time / tx)
local counter = otp.write_uint64_be(ct)
assert( #secret == 20 )
assert( otp.basexx.to_base64(secret) == "b5JlMIqmiePBi1tYlQYxNWGsqr8=" )
assert( #counter == 8 )
assert( otp.basexx.to_base64(counter) == "AAAAAANCp0A=" )
local hmac = otp.hmac(secret, counter)
assert(#hmac == 20)
for i=1,20 do
assert( string.byte(expected_hmac,i) == string.byte(hmac, i) )
end
callback()
end)
mtt.register("otp.generate_totp", function(callback)
local expected_code = "699847"
local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7"
local unix_time = 1640995200
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_b32)
print("Current code: " .. code .. " valid for " .. valid_seconds .. " seconds")
callback()
end)
mtt.register("otp.check_code", function(callback)
local expected_code = "699847"
local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7"
local unix_time = 1640995200
assert(otp.check_code(secret_b32, expected_code, unix_time))
assert(otp.check_code(secret_b32, expected_code, unix_time+30))
assert(otp.check_code(secret_b32, expected_code, unix_time-30))
assert(not otp.check_code(secret_b32, expected_code, unix_time-60))
assert(not otp.check_code(secret_b32, expected_code, unix_time+60))
assert(not otp.check_code(secret_b32, expected_code))
callback()
end)
mtt.register("otp.create_qr_png", function(callback)
local url = "otpauth://totp/abc:myaccount?algorithm=SHA1&digits=6&issuer=abc&period=30&"
.. "secret=N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7"
local ok, code = otp.qrcode(url)
assert(ok)
assert(code)
local png = otp.create_qr_png(code)
assert(png)
local f = io.open(minetest.get_worldpath() .. "/qr.png", "w")
f:write(png)
f:close()
callback()
end)
mtt.register("otp.generate_secret", function(callback)
local s = otp.generate_secret()
assert(#s == 20)
callback()
end)

View file

@ -1,25 +1,28 @@
local MP = minetest.get_modpath("otp") local MP = minetest.get_modpath("fediauth")
local http = minetest.request_http_api()
-- sanity checks -- sanity checks
assert(type(minetest.encode_png) == "function") assert(type(minetest.encode_png) == "function")
otp = { fediauth = {
-- mod storage -- mod storage
storage = minetest.get_mod_storage(), storage = minetest.get_mod_storage(),
-- baseXX functions -- baseXX functions
basexx = loadfile(MP.."/basexx.lua")(), basexx = loadfile(MP.."/basexx.lua")(),
-- qr code
qrcode = loadfile(MP.."/qrencode.lua")().qrcode
} }
dofile(MP.."/functions.lua") dofile(MP.."/mastoapi.lua")
dofile(MP.."/onboard.lua") local instance = minetest.settings:get("fediauth.instance")
dofile(MP.."/join.lua") local key = minetest.settings:get("fediauth.api_token")
dofile(MP.."/privs.lua") if not instance or not key then
dofile(MP.."/priv_revoke.lua") minetest.log("warning", "[fediauth] For working fediauth you should specify fediauth.instance and fediauth.api_token")
else
if minetest.get_modpath("mtt") and mtt.enabled then mastoapi_init(http, instance, key)
dofile(MP.."/functions.spec.lua") dofile(MP.."/functions.lua")
dofile(MP.."/onboard.lua")
dofile(MP.."/join.lua")
dofile(MP.."/privs.lua")
dofile(MP.."/priv_revoke.lua")
end end

137
join.lua
View file

@ -1,26 +1,73 @@
local FORMNAME = "otp-check" local FORMNAME = "fediauth-check"
local FORMNAMEFEDI = "fediauth-check-fedi"
-- time for otp code verification -- time for fediauth code verification
local otp_time = 300 local fediauth_time = 300
-- playername => start_time -- playername => start_time
local otp_sessions = {} local fediauth_sessions = {}
-- Code formspec on join for otp enabled players local formspecfediadd = "size[9,10]" ..
"label[1,7;Input your fediverse account handle]" ..
"image[1.5,0.6;7,7;fediverse.png]" ..
"field[1,9;4,1;fediverse_account_url;@nick@example.com;]" ..
"button[5,8.7;3,1;submit;Send code]"
local feditempstore = {}
minetest.register_entity("fediauth:checkmark", {
initial_properties = {
pointable = false,
armor_groups = { immortal = 1 },
visual = "sprite",
visual_size = {x = 0.5, y = 0.5},
textures = { "checkmark.png^[opacity:180" },
use_texture_alpha = true,
static_save = false,
glow = 5,
},
on_detach = function(self, parent) self.object:remove() end
})
function fediauth.verified_checkmark(player, verified)
local tag = player:get_player_name()
local props = player:get_properties()
if verified then
local obj = minetest.add_entity({x=0,y=2,z=0}, "fediauth:checkmark", nil)
obj:set_attach(player, "Head", {x = 0, y = 12, z = 0})
player:set_properties({nametag = props.nametag .. " [FEDI]", nametag_color = "#00ff00" })
end
end
-- Code formspec on join for fediauth enabled players
minetest.register_on_joinplayer(function(player) minetest.register_on_joinplayer(function(player)
local playername = player:get_player_name() local playername = player:get_player_name()
if otp.is_player_enabled(playername) then if fediauth.is_player_bypassed(playername) then return end
minetest.log("action", "[otp] session start for player: '" .. playername .. "'") if fediauth.is_player_enabled(playername) or minetest.settings:get_bool("fediauth.fedi_required", false) then
minetest.log("action", "[fediauth] session start for player: '" .. playername .. "'")
-- start otp session time -- start fediauth session time
otp_sessions[player:get_player_name()] = os.time() fediauth_sessions[player:get_player_name()] = os.time()
-- revoke important privs and re-grant again on code-verification -- revoke important privs and re-grant again on code-verification
otp.revoke_privs(playername) fediauth.revoke_privs(playername)
-- if fedi only allowed
if minetest.settings:get_bool("fediauth.fedi_required", false) then
local existsfedi = fediauth.storage:get_string(playername .. "_fedi")
if existsfedi == "" or not existsfedi then
minetest.log("action", "[fediauth] request fedi account for player: '" .. playername .. "'")
minetest.show_formspec(playername, FORMNAMEFEDI, formspecfediadd)
return
end
end
local secret_b32 = fediauth.get_player_secret_b32(playername)
local codeseq = fediauth.give_code(secret_b32)
fediauth.send_code(codeseq[1], fediauth.storage:get_string(playername .. "_fedi"))
-- send verification formspec -- send verification formspec
local formspec = "size[10,2]" .. local formspec = "size[10,2]" ..
"label[1,0;Please enter your OTP code below]" .. "label[1,0;Please check your fedi account and enter code]" ..
"field[1,1.3;4,1;code;Code;]" .. "field[1,1.3;4,1;code;Code;]" ..
"button_exit[5,1;3,1;submit;Verify]" "button_exit[5,1;3,1;submit;Verify]"
@ -28,39 +75,73 @@ minetest.register_on_joinplayer(function(player)
end end
end) end)
-- clear otp session on leave -- clear fediauth session on leave
minetest.register_on_leaveplayer(function(player) minetest.register_on_leaveplayer(function(player)
local playername = player:get_player_name() local playername = player:get_player_name()
otp_sessions[playername] = nil fediauth_sessions[playername] = nil
end) end)
-- check sessions periodically and kick if timed out -- check sessions periodically and kick if timed out
local function session_check() local function session_check()
local now = os.time() local now = os.time()
for name, start_time in pairs(otp_sessions) do for name, start_time in pairs(fediauth_sessions) do
if (now - start_time) > otp_time then if (now - start_time) > fediauth_time then
minetest.kick_player(name, "OTP Code validation timed out") minetest.kick_player(name, "fediauth code validation timed out")
otp_sessions[name] = nil fediauth_sessions[name] = nil
end end
end end
minetest.after(5, session_check) minetest.after(5, session_check)
end end
minetest.after(5, session_check) minetest.after(5, session_check)
-- otp check -- fediauth check
minetest.register_on_player_receive_fields(function(player, formname, fields) minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= FORMNAME then if formname ~= FORMNAME and formname ~= FORMNAMEFEDI then
return return
end end
local playername = player:get_player_name() local playername = player:get_player_name()
local secret_b32 = otp.get_player_secret_b32(playername) local secret_b32 = fediauth.get_player_secret_b32(playername)
if otp.check_code(secret_b32, fields.code) then
minetest.chat_send_player(playername, "OTP Code validation succeeded") -- check for new player or doesn't have fedi account
otp_sessions[playername] = nil if fields.fediverse_account_url then
otp.regrant_privs(playername) if not string.starts(fields.fediverse_account_url, "@") or string.len(fields.fediverse_account_url) < 3 or string.len(fields.fediverse_account_url) > 100 then
else minetest.chat_send_player(playername, minetest.colorize("#ff0000", "Try again, your input is incorrect"))
minetest.kick_player(playername, "OTP Code validation failed") minetest.show_formspec(playername, FORMNAMEFEDI, formspecfediadd)
otp.regrant_privs(playername) return
end
local secret_b32 = fediauth.get_player_secret_b32(playername)
local codeseq = fediauth.give_code(secret_b32)
fediauth.send_code(codeseq[1], fields.fediverse_account_url)
feditempstore[playername] = fields.fediverse_account_url
local formspec = "size[9,10]" ..
"label[1,7;Check code on " .. minetest.formspec_escape(fields.fediverse_account_url) .. "]" ..
"field[1,9;4,1;code;Code;]" ..
"button_exit[5,8.7;3,1;submit;Verify]"
minetest.show_formspec(playername, FORMNAME, formspec)
return
end end
end)
if fediauth.check_code(secret_b32, fields.code) then
local fedi_account = fediauth.storage:get_string(playername .. "_fedi")
-- for account without fediverse (for prevent write account if code incorrect
if fedi_account == "" and feditempstore[playername] then
fediauth.storage:set_string(playername .. "_fedi", feditempstore[playername])
fedi_account = feditempstore[playername]
feditempstore[playername] = nil
end
minetest.chat_send_player(playername, minetest.colorize("#00ff00", "fediauth code validation succeeded for " .. fedi_account))
fediauth_sessions[playername] = nil
fediauth.regrant_privs(playername)
fediauth.verified_checkmark(player, true)
else
minetest.kick_player(playername, "fediauth code validation failed")
fediauth.regrant_privs(playername)
end
end)

View file

@ -1,25 +1,122 @@
License of source code Creative Commons Legal Code
----------------------
The MIT License (MIT) CC0 1.0 Universal
Copyright (C) 2023 BuckarooBanzay
Permission is hereby granted, free of charge, to any person obtaining a copy of this CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
software and associated documentation files (the "Software"), to deal in the Software LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
without restriction, including without limitation the rights to use, copy, modify, merge, ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
publish, distribute, sublicense, and/or sell copies of the Software, and to permit INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
persons to whom the Software is furnished to do so, subject to the following conditions: REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
The above copyright notice and this permission notice shall be included in all copies or Statement of Purpose
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, The laws of most jurisdictions throughout the world automatically confer
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR exclusive Copyright and Related Rights (defined below) upon the creator
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE and subsequent owner(s) (each and all, an "owner") of an original work of
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR authorship and/or a database (each, a "Work").
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
For more details: Certain owners wish to permanently relinquish those rights to a Work for
https://opensource.org/licenses/MIT the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

27
mastoapi.lua Normal file
View file

@ -0,0 +1,27 @@
local http, instance, key
function fediauth.send_code(code, account_handle)
local status = {
visibility = "direct",
status = "" .. code .. " " .. account_handle .. " code for minetest fediauth, do not share it!"
}
local json = minetest.write_json(status)
http.fetch({
url = "https://" .. instance .. "/api/v1/statuses",
extra_headers = { "Content-Type: application/json", "Authorization: Bearer " .. key },
timeout = 15,
post_data = json
}, function(res)
if res then
minetest.log("action", "[fediauth] code sent to: '" .. account_handle .. "'")
else
minetest.log("error", "[fediauth] code not sent to: '" .. account_handle .. "'")
end
end)
end
function mastoapi_init(h, i, k)
http = h
instance = i
key = k
end

View file

@ -1,3 +1,2 @@
name = otp name = fediauth
optional_depends = mtt min_minetest_version = 5.3
min_minetest_version = 5.3

View file

@ -1,78 +1,81 @@
local FORMNAME = "otp-onboard" local FORMNAME = "fediauth-onboard"
local FORMNAMEFEDI = "fediauth-onboard-fedi"
minetest.register_chatcommand("otp_disable", { local feditempstore = {}
description = "Disable the otp verification",
privs = { otp_enabled = true, interact = true }, function string.starts(String,Start)
return string.sub(String,1,string.len(Start))==Start
end
minetest.register_chatcommand("fediauth_off", {
description = "Disable the fediauth verification",
privs = { fediauth_enabled = true, interact = true },
func = function(name) func = function(name)
-- clear priv -- clear priv
local privs = minetest.get_player_privs(name) local privs = minetest.get_player_privs(name)
privs.otp_enabled = nil privs.fediauth_enabled = nil
minetest.set_player_privs(name, privs) minetest.set_player_privs(name, privs)
return true, "OTP login disabled" return true, "fediauth login disabled"
end end
}) })
minetest.register_chatcommand("otp_enable", { minetest.register_chatcommand("fediauth_on", {
description = "Enable the otp verification", description = "Enable the fediauth verification",
func = function(name) func = function(name)
-- issuer name local secret_b32 = fediauth.get_player_secret_b32(name)
local issuer = minetest.settings:get("otp.issuer") or "Minetest"
local server_name = minetest.settings:get("server_name") local formspec_account = "size[9,10]" ..
local server_address = minetest.settings:get("server_address") "label[1,7;Input your fediverse account handle]" ..
if server_name and server_name ~= "" then "image[1.5,0.6;7,7;fediverse.png]" ..
issuer = server_name "field[1,9;4,1;fediverse_account_url;@nick@example.com;]" ..
elseif server_address and server_address ~= "" then "button[5,8.7;3,1;submit;Send code]"
issuer = server_address
minetest.show_formspec(name, FORMNAMEFEDI, formspec_account)
end end
-- authenticator image
local image = minetest.settings:get("otp.authenticator_image") or
"https://raw.githubusercontent.com/minetest/minetest/master/misc/minetest-xorg-icon-128.png"
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 ..
"&image=" .. image
local ok, code = otp.qrcode(url)
if not ok then
return false, "qr code generation failed"
end
local png = otp.create_qr_png(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
}) })
minetest.register_on_player_receive_fields(function(player, formname, fields) minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= FORMNAME then if formname ~= FORMNAME and formname ~= FORMNAMEFEDI then
return return
end end
if fields.fediverse_account_url then
local playername = player:get_player_name()
if not string.starts(fields.fediverse_account_url, "@") or string.len(fields.fediverse_account_url) < 3 or string.len(fields.fediverse_account_url) > 100 then
minetest.chat_send_player(playername, minetest.colorize("#ff0000", "Try again, your input is incorrect"))
return
end
local secret_b32 = fediauth.get_player_secret_b32(playername)
local codeseq = fediauth.give_code(secret_b32)
fediauth.send_code(codeseq[1], fields.fediverse_account_url)
feditempstore[playername] = fields.fediverse_account_url
local formspec = "size[9,10]" ..
"label[1,7;Check code on " .. minetest.formspec_escape(fields.fediverse_account_url) .. "]" ..
"field[1,9;4,1;code;Code;]" ..
"button_exit[5,8.7;3,1;submit;Verify]"
minetest.show_formspec(playername, FORMNAME, formspec)
end
if fields.code then if fields.code then
local playername = player:get_player_name() local playername = player:get_player_name()
local secret_b32 = otp.get_player_secret_b32(playername) local secret_b32 = fediauth.get_player_secret_b32(playername)
if otp.check_code(secret_b32, fields.code) then if fediauth.check_code(secret_b32, fields.code) then
-- set priv -- set priv
local privs = minetest.get_player_privs(playername) local privs = minetest.get_player_privs(playername)
privs.otp_enabled = true privs.fediauth_enabled = true
minetest.set_player_privs(playername, privs) minetest.set_player_privs(playername, privs)
fediauth.verified_checkmark(player, true)
if feditempstore[playername] then
fediauth.storage:set_string(playername .. "_fedi", feditempstore[playername])
feditempstore[playername] = nil
end
minetest.chat_send_player(playername, "Code validation succeeded, OTP login enabled") minetest.chat_send_player(playername, "Code validation succeeded, fediauth login enabled")
else else
minetest.chat_send_player(playername, "Code validation failed!") minetest.chat_send_player(playername, "Code validation failed!")
end end
end end
end) end)

View file

@ -2,18 +2,18 @@
-- privs to revoke until the verification code is validated -- privs to revoke until the verification code is validated
local temp_revoke_privs = {} local temp_revoke_privs = {}
-- mark player health-related privs as "otp_keep" (they don't get removed while entering the otp code) -- mark player health-related privs as "fediauth_keep" (they don't get removed while entering the fediauth code)
for _, name in ipairs({"fly", "noclip"}) do for _, name in ipairs({"fly", "noclip"}) do
local priv_def = minetest.registered_privileges[name] local priv_def = minetest.registered_privileges[name]
if priv_def then if priv_def then
priv_def.otp_keep = true priv_def.fediauth_keep = true
end end
end end
minetest.register_on_mods_loaded(function() minetest.register_on_mods_loaded(function()
-- collect all privs to revoke while entering the otp code -- collect all privs to revoke while entering the fediauth code
for name, priv_def in pairs(minetest.registered_privileges) do for name, priv_def in pairs(minetest.registered_privileges) do
if not priv_def.otp_keep then if not priv_def.fediauth_keep then
-- not marked explicitly as "keep" -- not marked explicitly as "keep"
table.insert(temp_revoke_privs, name) table.insert(temp_revoke_privs, name)
end end
@ -21,9 +21,9 @@ minetest.register_on_mods_loaded(function()
end) end)
-- moves all "temp_revoke_privs" to mod-storage -- moves all "temp_revoke_privs" to mod-storage
function otp.revoke_privs(playername) function fediauth.revoke_privs(playername)
local privs = minetest.get_player_privs(playername) local privs = minetest.get_player_privs(playername)
if otp.storage:get_string(playername .. "_privs") == "" then if fediauth.storage:get_string(playername .. "_privs") == "" then
local moved_privs = {} local moved_privs = {}
for _, priv_name in ipairs(temp_revoke_privs) do for _, priv_name in ipairs(temp_revoke_privs) do
@ -33,15 +33,15 @@ function otp.revoke_privs(playername)
end end
end end
minetest.log("action", "[otp] revoking privs of '" .. playername .. "' list: " .. dump(moved_privs)) minetest.log("action", "[fediauth] revoking privs of '" .. playername .. "' list: " .. dump(moved_privs))
minetest.set_player_privs(playername, privs) minetest.set_player_privs(playername, privs)
otp.storage:set_string(playername .. "_privs", minetest.serialize(moved_privs)) fediauth.storage:set_string(playername .. "_privs", minetest.serialize(moved_privs))
end end
end end
-- moves all privs from mod-storage into the live privs -- moves all privs from mod-storage into the live privs
function otp.regrant_privs(playername) function fediauth.regrant_privs(playername)
local stored_priv_str = otp.storage:get_string(playername .. "_privs") local stored_priv_str = fediauth.storage:get_string(playername .. "_privs")
if stored_priv_str ~= "" then if stored_priv_str ~= "" then
local privs = minetest.get_player_privs(playername) local privs = minetest.get_player_privs(playername)
local stored_privs = minetest.deserialize(stored_priv_str) local stored_privs = minetest.deserialize(stored_priv_str)
@ -51,8 +51,8 @@ function otp.regrant_privs(playername)
privs[priv_name] = true privs[priv_name] = true
end end
minetest.log("action", "[otp] regranting privs of '" .. playername .. "' list: " .. dump(stored_privs)) minetest.log("action", "[fediauth] regranting privs of '" .. playername .. "' list: " .. dump(stored_privs))
minetest.set_player_privs(playername, privs) minetest.set_player_privs(playername, privs)
otp.storage:set_string(playername .. "_privs", "") fediauth.storage:set_string(playername .. "_privs", "")
end end
end end

View file

@ -1,6 +1,12 @@
minetest.register_privilege("otp_enabled", { minetest.register_privilege("fediauth_enabled", {
description = "otp enabled player", description = "fediauth enabled player",
give_to_singleplayer = false, give_to_singleplayer = false,
otp_keep = true fediauth_keep = true
})
minetest.register_privilege("fediauth_bypass", {
description = "fediauth bypass for players who not want type code on each log in (when enabled fediauth.fedi_required)",
give_to_singleplayer = false,
fediauth_keep = true
}) })

File diff suppressed because it is too large Load diff

View file

@ -1,49 +1,50 @@
# (T)OTP mod for minetest # FediAuth mod for minetest
* State: **Stable** 2FA via Fediverse account, based on https://content.minetest.net/packages/mt-mods/otp/
# Overview # Overview
Lets security-aware players use the `/otp_enable` command to protect their account with a second factor. Lets Fediverse players use the `/fediauth_on` 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. Players that have the FediAuth enabled have to enter a verification code upon joining the game, the code will be sent to their account handle (@nick@example.com).
# OTP Authenticator apps That mod requires add to `secure.http_mods = fediauth` for sending codes from service account (any mastodon API compatible instance)
* https://freeotp.github.io/ Add `fediauth.instance = example.com` and `fediauth.api_token` = secret` for work this mod.
* https://github.com/helloworld1/FreeOTPPlus
Also you can enable fediauth.fedi_required option and players who not have fediverse account can't play on server
# Screenshots # Screenshots
OTP verification form FediAuth verification form
![](./screenshot1.png) ![](./screenshot1.jpg)
FediAuth Setup form
![](./screenshot2.jpg)
FediAuth checkmark if verified success
![](./screenshot3.jpg)
OTP Setup form
![](./screenshot2.png)
# Temporary privilege revocation # Temporary privilege revocation
All of the privileges get revoked when logging in with the otp enabled (until the proper code is entered). All of the privileges get revoked when logging in with the fediauth enabled (until the proper code is entered).
Some exceptions: Some exceptions:
* `fly` (otherwise the player would literally fall from the sky) * `fly` (otherwise the player would literally fall from the sky)
* `noclip` * `noclip`
To disable revokation on custom privs the field `otp_keep` can be set to true on the definition: To disable revokation on custom privs the field `fediauth_keep` can be set to true on the definition:
```lua ```lua
minetest.register_privilege("my_super_important_priv", { minetest.register_privilege("my_super_important_priv", {
description = "something something", description = "something something",
otp_keep = true fediauth_keep = true
}) })
``` ```
# Settings
* `otp.authenticator_image` The image to use in the QR code for the otp app (defaults to "https://raw.githubusercontent.com/minetest/minetest/master/misc/minetest-xorg-icon-128.png")
* `otp.issuer` The issuer name, defaults to server name, address or just "Minetest"
# Links / References # Links / References
* https://fedi.tips/
* https://en.wikipedia.org/wiki/Time-based_one-time_password * 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-based_one-time_password
* https://en.wikipedia.org/wiki/HMAC * https://en.wikipedia.org/wiki/HMAC
@ -51,16 +52,16 @@ minetest.register_privilege("my_super_important_priv", {
# Chatcommands # Chatcommands
* `/otp_enable` Starts the OTP onboarding process * `/fediauth_on` Starts the FediAuth
* `/otp_disable` Disables the OTP Login * `/fediauth_off` Disables the FediAuth login
# Privileges # Privileges
* `otp_enabled` Players with this privilege have to verify the OTP Code upon login (automatically granted on successful `/otp_enable`) * `fediauth_enabled` Players with this privilege have to verify the Fediverse code upon login (automatically granted on successful `/fediauth_enable`)
* `fediauth_bypass` Players with this privilege can bypass verification for any reason, and the privilege can only granted manually by administrator
# License # License
* Code: `MIT` * Code: `CC0-1.0`
* Textures: `CC-BY-SA 3.0` * Textures: `CC-BY-SA 3.0`
* "basexx.lua" `MIT` https://github.com/aiq/basexx/blob/master/lib/basexx.lua * "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

BIN
screenshot1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
screenshot2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

BIN
screenshot3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

9
settingtypes.txt Normal file
View file

@ -0,0 +1,9 @@
# Instance domain for service account (code sender)
# example.com
# Must be mastodon API compatible
fediauth.instance (Instance domain) string
fediauth.api_token (Token for account) string
# If no fediverse account - no access to server
fediauth.fedi_required (Require Fediverse account for each user) bool false

View file

@ -1,11 +0,0 @@
ARG ENGINE_VERSION=5.6.1
FROM registry.gitlab.com/minetest/minetest/server:${ENGINE_VERSION}
USER root
RUN apk add git &&\
mkdir -p /root/.minetest/worlds/world/worldmods/ &&\
cd /root/.minetest/worlds/world/worldmods/ &&\
git clone https://github.com/BuckarooBanzay/mtt
ENTRYPOINT minetestserver --config /minetest.conf

View file

@ -1,3 +0,0 @@
default_game = minetest_game
mg_name = v7
mtt_enable = true

BIN
textures/checkmark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/fediverse.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB