Return SRP-style hash from minetest.get_password_hash

Old documentation never made any requirements that the return value is stable across Minetest versions or deterministic. Also added some additional error checking to flag API misuse by mods.
This commit is contained in:
red-001 2024-08-20 01:25:20 +01:00
parent 6346b90027
commit abf7385f12
4 changed files with 94 additions and 11 deletions

View File

@ -6057,13 +6057,29 @@ Authentication
engine as returned as part of a `get_auth()` call on the auth handler.
* Only use this function for making it possible to log in via password from
external protocols such as IRC, other uses are frowned upon.
* `minetest.get_password_hash(name, raw_password)`
* `minetest.get_password_hash(name, password)`
* Convert a name-password pair to a password hash that Minetest can use.
* The returned value alone is not a good basis for password checks based
on comparing the password hash in the database with the password hash
from the function, with an externally provided password, as the hash
in the db might use the new SRP verifier format.
* For this purpose, use `minetest.check_password_entry` instead.
* Player name *cannot* be empty, and both arguments *must* be strings.
* Control codes are not allowed in either argument, and will result in a Lua error if included.
* The returned value may not be the same for multiple invocations with the same name and password.
* The returned value therefore *cannot* be used for password checks based.
on a direct string comparison, use `minetest.check_password_entry` instead.
* If you used this function as a hash you can replace it with `minetest.sha1`:
```lua
-- Lua equivalent of the legacy minetest.get_password_hash algorithm
local function legacy_get_password_hash(name, password)
if password == "" then
return ""
end
-- convert to strings
name = tostring(name)
password = tostring(password)
-- remove anything after the first null character
name = name:match("^[^\x00]+")
password = password:match("^[^\x00]+")
return minetest.sha1(tostring(name)..tostring(password))
end
```
* `minetest.get_player_ip(name)`: returns an IP address string for the player
`name`.
* The player needs to be online for this to be successful.

View File

@ -125,6 +125,37 @@ local function test_hashing()
end
unittests.register("test_hashing", test_hashing)
local function test_password_hash()
local password = "hunter2"
local user = "Cthon98"
local hash = core.get_password_hash(user, password)
local hash_no_pass = core.get_password_hash(user, "")
assert(hash)
assert(type(hash_no_pass) == "string" and hash_no_pass ~= "")
assert(hash_no_pass ~= hash)
assert(core.check_password_entry(user, hash, password) == true)
assert(core.check_password_entry(user, hash, "") == false)
assert(core.check_password_entry(user, hash_no_pass, password) == false)
assert(core.check_password_entry(user, hash_no_pass, "") == true)
assert(pcall(core.get_password_hash, "", password) == false)
assert(pcall(core.get_password_hash, "", 2) == false)
assert(pcall(core.get_password_hash, user, 2) == false)
assert(pcall(core.get_password_hash, 98, 2) == false)
assert(pcall(core.get_password_hash, 98, password) == false)
assert(pcall(core.get_password_hash, user) == false)
assert(pcall(core.get_password_hash) == false)
assert(pcall(core.get_password_hash, "Cthon\x01\x00\x12\x98", password) == false)
assert(pcall(core.get_password_hash, user, "hunter\x02") == false)
-- tab and any UTF/high ascii are allowed (if perhaps a bad idea)
pcall(core.get_password_hash, user.."\t\xE2\x9D\x93", password)
pcall(core.get_password_hash, user, password.."\xE2\x9D\x94\t")
end
unittests.register("test_password_hash", test_password_hash)
local function test_compress()
-- This text should be compressible, to make sure the results are... normal
local text = "The\000 icey canoe couldn't move very well on the\128 lake. The\000 ice was too stiff and the icey canoe's paddles simply wouldn't punch through."

View File

@ -46,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "util/png.h"
#include "player.h"
#include <cstdio>
#include <locale>
// only available in zstd 1.3.5+
#ifndef ZSTD_CLEVEL_DEFAULT
@ -238,14 +239,46 @@ int ModApiUtil::l_check_password_entry(lua_State *L)
return 1;
}
// get_password_hash(name, raw_password)
std::string_view ModApiUtil::read_string_strict(lua_State* L, int arg_index)
{
// only accept strings, not numbers
luaL_checktype(L, arg_index, LUA_TSTRING);
std::string_view s = readParam<std::string_view>(L, arg_index);
// check each character is either printable or "looks" like it could be utf-8
// Locale is set to avoid any nasty localisation-dependent bugs
std::locale locale_c("C");
bool bad_character_count = std::count_if(s.begin(), s.end(),
[&locale_c](unsigned char c)
{
return c <= 127 && !std::isprint(c, locale_c) && !std::isspace(c, locale_c);
}
);
luaL_argcheck(L, bad_character_count != 0, arg_index,
"string contained unexpected characters, only printable characters or whitespace are allowed!");
return s;
}
// get_password_hash(name, password)
int ModApiUtil::l_get_password_hash(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
std::string name = luaL_checkstring(L, 1);
std::string raw_password = luaL_checkstring(L, 2);
std::string hash = translate_password(name, raw_password);
lua_pushstring(L, hash.c_str());
std::string_view name = read_string_strict(L, 1);
luaL_argcheck(L, !name.empty(), 1,
"User name must be non-empty, use minetest.sha1 or minetest.sha256 if you need a generic hash function.\n"
"See documentation for more details on how to migrate legacy code.");
std::string_view password = read_string_strict(L, 2);
std::string verifier;
std::string salt;
generate_srp_verifier_and_salt(name, password, &verifier, &salt);
std::string hash = encode_srp_verifier(verifier, salt);
lua_pushlstring(L, hash.c_str(), hash.length());
return 1;
}

View File

@ -137,6 +137,9 @@ private:
// is_valid_player_name(name)
static int l_is_valid_player_name(lua_State *L);
// get a printable string with strong type checking
static std::string_view read_string_strict(lua_State* L, int arg_index);
public:
static void Initialize(lua_State *L, int top);
static void InitializeAsync(lua_State *L, int top);