-- big-endian uint64 of a number function fediauth.write_uint64_be(v) local b1 = bit.band( bit.rshift(v, 56), 0xFF ) local b2 = bit.band( bit.rshift(v, 48), 0xFF ) local b3 = bit.band( bit.rshift(v, 40), 0xFF ) local b4 = bit.band( bit.rshift(v, 32), 0xFF ) local b5 = bit.band( bit.rshift(v, 24), 0xFF ) local b6 = bit.band( bit.rshift(v, 16), 0xFF ) local b7 = bit.band( bit.rshift(v, 8), 0xFF ) local b8 = bit.band( bit.rshift(v, 0), 0xFF ) return string.char(b1, b2, b3, b4, b5, b6, b7, b8) end -- prepare paddings -- https://en.wikipedia.org/wiki/HMAC local i_pad = "" local o_pad = "" i_pad = string.char(0x36):rep(64) o_pad = string.char(0x5c):rep(64) -- hmac generation function fediauth.hmac(key, message) local i_key_pad = "" for i=1,64 do i_key_pad = i_key_pad .. string.char(bit.bxor(string.byte(key, i) or 0x00, string.byte(i_pad, i))) end assert(#i_key_pad == 64) local o_key_pad = "" for i=1,64 do o_key_pad = o_key_pad .. string.char(bit.bxor(string.byte(key, i) or 0x00, string.byte(o_pad, i))) end assert(#o_key_pad == 64) -- concat message local first_msg = i_key_pad for i=1,#message do first_msg = first_msg .. string.char(string.byte(message, i)) end assert(#first_msg == 64+8) -- hash first message local hash_sum_1 = minetest.sha1(first_msg, true) assert(#hash_sum_1 == 20) -- concat first message to second local second_msg = o_key_pad for i=1,#hash_sum_1 do second_msg = second_msg .. string.char(string.byte(hash_sum_1, i)) end assert(#second_msg == 64+20) local hmac = minetest.sha1(second_msg, true) assert(#hmac == 20) return hmac end local function left_pad(str, s, len) return s:rep(#str - len) .. str end 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 = fediauth.write_uint64_be(ct) local valid_seconds = ((ct * tx) + tx) - unix_time local hmac = fediauth.hmac(key, counter) -- https://www.rfc-editor.org/rfc/rfc4226#section-5.4 local offset = bit.band(string.byte(hmac, #hmac), 0xF) local value = 0 value = bit.bxor(value, string.byte(hmac, offset+4)) value = bit.bxor(value, bit.lshift(string.byte(hmac, offset+3), 8)) value = bit.bxor(value, bit.lshift(string.byte(hmac, offset+2), 16)) value = bit.bxor(value, bit.lshift(bit.band(string.byte(hmac, offset+1), 0x7F), 24)) local code = value % 10^6 local padded_code = left_pad("" .. code, "0", 6) return padded_code, valid_seconds end function fediauth.generate_secret() local buf = minetest.sha1("" .. math.random(10000), true) local s = buf:sub(1, 20) return s end -- get or generate per-player secret b32 ecoded function fediauth.get_player_secret_b32(name) local secret_b32 = fediauth.storage:get_string(name .. "_secret") if secret_b32 == "" then 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 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 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, -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 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 function fediauth.is_home_instance(domain) local current_instance_service = minetest.settings:get("fediauth.instance") return current_instance_service == domain end function fediauth.check_for_restricted_instance(domain) local restricted_instances = minetest.settings:get("fediauth.restricted_instances") or {} if type(restricted_instances) == "string" then restricted_instances = restricted_instances:split(",") end for _, instance in ipairs(restricted_instances) do if instance == domain then minetest.log("action", "[fediauth] domain restricted: '" .. domain .. "'") return true end end return false end