2020-07-05 08:30:14 -04:00

182 lines
6.4 KiB
Lua

-- LUALOCALS < ---------------------------------------------------------
local error, getmetatable, math, minetest, os, pcall
= error, getmetatable, math, minetest, os, pcall
local math_random, os_time
= math.random, os.time
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
------------------------------------------------------------------------
-- SALT AND HASH GENERATION
-- Minetest password hashes (and password hashes in general) should have a
-- fixed length, though the actual length may be subject to change in
-- future versions.
local hashlen = minetest.get_password_hash("a", "b"):len()
-- Helper function to generate a new, random(-ish) salt value. The quality
-- of the random source is questionable, but it's probably the best we have
-- reliable access to here.
local function gensalt()
local alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
local salt = ""
while salt:len() < hashlen do
local n = math_random(1, alpha:len())
salt = salt .. alpha:sub(n, n)
end
return salt
end
-- Helper function to automatically upgrade non-secure un-hashed passwords
-- to hashed versions, using a new, random(-ish) salt. The old, unencrypted
-- password is replaced with a "~" string to indicate that it has already
-- been converted to a hash (using ~ instead of empty string so we can set
-- the password to empty-string to disable this feature).
local function upgradepass(changed)
local rawpass = minetest.settings:get(modname .. "_password")
if rawpass and rawpass ~= "~" then
local newsalt = ""
local newhash = ""
if rawpass ~= "~" then
newsalt = gensalt()
newhash = minetest.get_password_hash(newsalt, rawpass)
end
minetest.settings:set(modname .. "_password", "~")
minetest.settings:set(modname .. "_password_salt", newsalt)
minetest.settings:set(modname .. "_password_hash", newhash)
if changed then return changed() end
end
end
-- On initial startup, if there's an unsafe password set in the config
-- file, upgrade it automatically, and save the upgraded config so it's
-- not exposed on disk, e.g. in backups.
upgradepass(function() minetest.settings:write() end)
------------------------------------------------------------------------
-- PROTECT PRIVILEGE-RELATED SETTINGS
-- Try to wrap the built-in "set" chat command, so that:
-- - Changing the su password will trigger a re-hash.
-- - Users with "server" privs (who can use /set) but without "privs"
-- privs cannot exploit certain known settings to gain "privs" access.
if minetest.chatcommands and minetest.chatcommands.set
and minetest.chatcommands.set.func then
local prefix = modname .. "_"
local oldfunc = minetest.chatcommands.set.func
minetest.chatcommands.set.func = function(name, ...)
-- If the server user also has privs access, just allow the
-- setting change, and rehash as needed.
if minetest.check_player_privs(name, {privs = true}) then
local function postset(...)
upgradepass()
return ...
end
return postset(oldfunc(name, ...))
end
-- Wrap the built-in setting modification function to block
-- certain settings from being set during the execution of
-- this command.
local setmeta = getmetatable(minetest.settings) or minetest.settings
local oldset = setmeta.set
setmeta.set = function(obj, setting, ...)
if setting and (setting == "name"
or setting:sub(1, prefix:len()) == prefix) then
error("NEEDPRIVS")
end
return oldset(obj, setting, ...)
end
-- Helper to handle result of command pcall; report our custom
-- error, bubble out other errors, otherwise return normally.
-- Restore the normal setting modification API after the command.
local function postset(ok, err, ...)
setmeta.set = oldset
if ok then
return err, ...
else
if not err:find("NEEDPRIVS") then
error(err)
end
return false, "Some settings require additional privileges to set."
end
end
return postset(pcall(oldfunc, name, ...))
end
end
------------------------------------------------------------------------
-- REGISTER CHAT COMMANDS
-- Helper function to add/remove the "privs" priviledge for a user.
local function changeprivs(name, priv)
local privs = minetest.get_player_privs(name)
privs.privs = priv
minetest.set_player_privs(name, privs)
return true, "Privileges of " .. name .. ": "
.. minetest.privs_to_string(minetest.get_player_privs(name))
end
-- Keep track of last attempt, and apply a short delay to rate-limit
-- players trying to brute-force passwords.
local retry = {}
-- Register /su command to escalate privs by password. The argument is the
-- password, which must match the one configured. If no password is configured,
-- then the command will always return failure.
minetest.register_chatcommand("su", {
description = "Escalate privileges by password.",
func = function(name, pass)
-- Check for already admin.
if minetest.check_player_privs(name, {privs = true}) then
return false, "You are already a superuser."
end
-- Check rate limit.
local now = os_time()
if retry[name] and now < (retry[name] + 5) then
return false, "Wait a few seconds before trying again."
end
retry[name] = now
-- Check password.
local hash = minetest.settings:get(modname .. "_password_hash")
local salt = minetest.settings:get(modname .. "_password_salt")
if not pass or pass == ""
or not hash or hash == ""
or not salt or salt == ""
or minetest.get_password_hash(salt, pass) ~= hash then
return false, "Authentication failure."
end
return changeprivs(name, true)
end
})
-- A shortcut to exit "su mode"; this is really just a shortcut for
-- "/revoke <me> privs", which escalated users will be able to do.
minetest.register_chatcommand("unsu", {
description = "Shortcut to de-escalate privileges from su.",
privs = {privs = true},
func = function(name) return changeprivs(name) end
})
------------------------------------------------------------------------
-- STRICT MODE ENFORCEMENT
-- Allow a "strict" setting to be set, which requires players to use
-- /su to escalate to admin after login, i.e. their privs are NOT persisted
-- after logout.
local function strictenforce(player)
if not minetest.settings:get_bool(modname .. "_strict") then return end
if not player then return end
local name = player:get_player_name()
if not name then return end
return changeprivs(name)
end
minetest.register_on_joinplayer(strictenforce)
minetest.register_on_leaveplayer(strictenforce)