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 = {
"otp",
"fediauth",
"minetest"
}
@ -13,10 +13,5 @@ read_globals = {
"dump", "dump2",
"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
-- 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 b2 = bitand( rshift(v, 48), 0xFF )
local b3 = bitand( rshift(v, 40), 0xFF )
@ -73,7 +73,7 @@ for _=1,64 do
end
-- hmac generation
function otp.hmac(key, message)
function fediauth.hmac(key, message)
local i_key_pad = ""
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)))
@ -117,16 +117,16 @@ local function left_pad(str, s, len)
return str
end
function otp.generate_totp(secret_b32, unix_time)
local key = otp.basexx.from_base32(secret_b32)
function fediauth.generate_tfediauth(secret_b32, unix_time)
local key = fediauth.basexx.from_base32(secret_b32)
unix_time = unix_time or os.time()
local tx = 30
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 hmac = otp.hmac(key, counter)
local hmac = fediauth.hmac(key, counter)
-- https://www.rfc-editor.org/rfc/rfc4226#section-5.4
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
end
function otp.create_qr_png(data)
function fediauth.create_qr_png(data)
local height = #data + 2
local width = height
@ -174,7 +174,7 @@ function otp.create_qr_png(data)
return minetest.encode_png(width, height, png_data, 2)
end
function otp.generate_secret()
function fediauth.generate_secret()
local buf = minetest.sha1("" .. math.random(10000), true)
local s = ""
for i=1,20 do
@ -184,30 +184,46 @@ function otp.generate_secret()
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")
function fediauth.get_player_secret_b32(name)
local secret_b32 = fediauth.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)
secret_b32 = fediauth.basexx.to_base32(fediauth.generate_secret())
fediauth.storage:set_string(name .. "_secret", secret_b32)
end
return secret_b32
end
-- returns true if the player is otp enabled _and_ set up properly
function otp.is_player_enabled(name)
local has_secret = otp.storage:get_string(name .. "_secret") ~= ""
local has_priv = minetest.check_player_privs(name, "otp_enabled")
-- returns true if the player is fediauth enabled _and_ set up properly
function fediauth.is_player_enabled(name)
local has_secret = fediauth.storage:get_string(name .. "_secret") ~= ""
local has_priv = minetest.check_player_privs(name, "fediauth_enabled")
return has_secret and has_priv
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()
for _, t_offset in ipairs({0, -30, 30}) do
local expected_code = otp.generate_totp(secret_b32, time + t_offset)
for _, t_offset in ipairs({0, -30, 30, -60, 60}) do
local expected_code = fediauth.generate_tfediauth(secret_b32, time + t_offset)
if expected_code == code then
return true
end
end
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
assert(type(minetest.encode_png) == "function")
otp = {
fediauth = {
-- mod storage
storage = minetest.get_mod_storage(),
-- baseXX functions
basexx = loadfile(MP.."/basexx.lua")(),
-- qr code
qrcode = loadfile(MP.."/qrencode.lua")().qrcode
}
dofile(MP.."/functions.lua")
dofile(MP.."/onboard.lua")
dofile(MP.."/join.lua")
dofile(MP.."/privs.lua")
dofile(MP.."/priv_revoke.lua")
if minetest.get_modpath("mtt") and mtt.enabled then
dofile(MP.."/functions.spec.lua")
dofile(MP.."/mastoapi.lua")
local instance = minetest.settings:get("fediauth.instance")
local key = minetest.settings:get("fediauth.api_token")
if not instance or not key then
minetest.log("warning", "[fediauth] For working fediauth you should specify fediauth.instance and fediauth.api_token")
else
mastoapi_init(http, instance, key)
dofile(MP.."/functions.lua")
dofile(MP.."/onboard.lua")
dofile(MP.."/join.lua")
dofile(MP.."/privs.lua")
dofile(MP.."/priv_revoke.lua")
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
local otp_time = 300
-- time for fediauth code verification
local fediauth_time = 300
-- 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)
local playername = player:get_player_name()
if otp.is_player_enabled(playername) then
minetest.log("action", "[otp] session start for player: '" .. playername .. "'")
if fediauth.is_player_bypassed(playername) then return end
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
otp_sessions[player:get_player_name()] = os.time()
-- start fediauth session time
fediauth_sessions[player:get_player_name()] = os.time()
-- 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
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;]" ..
"button_exit[5,1;3,1;submit;Verify]"
@ -28,39 +75,73 @@ minetest.register_on_joinplayer(function(player)
end
end)
-- clear otp session on leave
-- clear fediauth session on leave
minetest.register_on_leaveplayer(function(player)
local playername = player:get_player_name()
otp_sessions[playername] = nil
fediauth_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
for name, start_time in pairs(fediauth_sessions) do
if (now - start_time) > fediauth_time then
minetest.kick_player(name, "fediauth code validation timed out")
fediauth_sessions[name] = nil
end
end
minetest.after(5, session_check)
end
minetest.after(5, session_check)
-- otp check
-- fediauth check
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= FORMNAME then
if formname ~= FORMNAME and formname ~= FORMNAMEFEDI then
return
end
local playername = player:get_player_name()
local secret_b32 = otp.get_player_secret_b32(playername)
if otp.check_code(secret_b32, fields.code) then
minetest.chat_send_player(playername, "OTP Code validation succeeded")
otp_sessions[playername] = nil
otp.regrant_privs(playername)
else
minetest.kick_player(playername, "OTP Code validation failed")
otp.regrant_privs(playername)
local secret_b32 = fediauth.get_player_secret_b32(playername)
-- check for new player or doesn't have fedi account
if fields.fediverse_account_url then
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"))
minetest.show_formspec(playername, FORMNAMEFEDI, formspecfediadd)
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)
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)
Copyright (C) 2023 BuckarooBanzay
CC0 1.0 Universal
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
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
substantial portions of the Software.
Statement of Purpose
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
For more details:
https://opensource.org/licenses/MIT
Certain owners wish to permanently relinquish those rights to a Work for
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
optional_depends = mtt
min_minetest_version = 5.3
name = fediauth
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", {
description = "Disable the otp verification",
privs = { otp_enabled = true, interact = true },
local feditempstore = {}
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)
-- clear priv
local privs = minetest.get_player_privs(name)
privs.otp_enabled = nil
privs.fediauth_enabled = nil
minetest.set_player_privs(name, privs)
return true, "OTP login disabled"
return true, "fediauth login disabled"
end
})
minetest.register_chatcommand("otp_enable", {
description = "Enable the otp verification",
minetest.register_chatcommand("fediauth_on", {
description = "Enable the fediauth verification",
func = function(name)
-- issuer name
local issuer = minetest.settings:get("otp.issuer") or "Minetest"
local server_name = minetest.settings:get("server_name")
local server_address = minetest.settings:get("server_address")
if server_name and server_name ~= "" then
issuer = server_name
elseif server_address and server_address ~= "" then
issuer = server_address
local secret_b32 = fediauth.get_player_secret_b32(name)
local formspec_account = "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]"
minetest.show_formspec(name, FORMNAMEFEDI, formspec_account)
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)
if formname ~= FORMNAME then
if formname ~= FORMNAME and formname ~= FORMNAMEFEDI then
return
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
local playername = player:get_player_name()
local secret_b32 = otp.get_player_secret_b32(playername)
if otp.check_code(secret_b32, fields.code) then
local secret_b32 = fediauth.get_player_secret_b32(playername)
if fediauth.check_code(secret_b32, fields.code) then
-- set priv
local privs = minetest.get_player_privs(playername)
privs.otp_enabled = true
privs.fediauth_enabled = true
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
minetest.chat_send_player(playername, "Code validation failed!")
end
end
end)
end)

View file

@ -2,18 +2,18 @@
-- privs to revoke until the verification code is validated
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
local priv_def = minetest.registered_privileges[name]
if priv_def then
priv_def.otp_keep = true
priv_def.fediauth_keep = true
end
end
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
if not priv_def.otp_keep then
if not priv_def.fediauth_keep then
-- not marked explicitly as "keep"
table.insert(temp_revoke_privs, name)
end
@ -21,9 +21,9 @@ minetest.register_on_mods_loaded(function()
end)
-- 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)
if otp.storage:get_string(playername .. "_privs") == "" then
if fediauth.storage:get_string(playername .. "_privs") == "" then
local moved_privs = {}
for _, priv_name in ipairs(temp_revoke_privs) do
@ -33,15 +33,15 @@ function otp.revoke_privs(playername)
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)
otp.storage:set_string(playername .. "_privs", minetest.serialize(moved_privs))
fediauth.storage:set_string(playername .. "_privs", minetest.serialize(moved_privs))
end
end
-- moves all privs from mod-storage into the live privs
function otp.regrant_privs(playername)
local stored_priv_str = otp.storage:get_string(playername .. "_privs")
function fediauth.regrant_privs(playername)
local stored_priv_str = fediauth.storage:get_string(playername .. "_privs")
if stored_priv_str ~= "" then
local privs = minetest.get_player_privs(playername)
local stored_privs = minetest.deserialize(stored_priv_str)
@ -51,8 +51,8 @@ function otp.regrant_privs(playername)
privs[priv_name] = true
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)
otp.storage:set_string(playername .. "_privs", "")
fediauth.storage:set_string(playername .. "_privs", "")
end
end

View file

@ -1,6 +1,12 @@
minetest.register_privilege("otp_enabled", {
description = "otp enabled player",
minetest.register_privilege("fediauth_enabled", {
description = "fediauth enabled player",
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
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/
* https://github.com/helloworld1/FreeOTPPlus
Add `fediauth.instance = example.com` and `fediauth.api_token` = secret` for work this mod.
Also you can enable fediauth.fedi_required option and players who not have fediverse account can't play on server
# Screenshots
OTP verification form
![](./screenshot1.png)
FediAuth verification form
![](./screenshot1.jpg)
FediAuth Setup form
![](./screenshot2.jpg)
FediAuth checkmark if verified success
![](./screenshot3.jpg)
OTP Setup form
![](./screenshot2.png)
# 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:
* `fly` (otherwise the player would literally fall from the sky)
* `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
minetest.register_privilege("my_super_important_priv", {
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
* https://fedi.tips/
* 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
@ -51,16 +52,16 @@ minetest.register_privilege("my_super_important_priv", {
# Chatcommands
* `/otp_enable` Starts the OTP onboarding process
* `/otp_disable` Disables the OTP Login
* `/fediauth_on` Starts the FediAuth
* `/fediauth_off` Disables the FediAuth login
# 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
* Code: `MIT`
* Code: `CC0-1.0`
* 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

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