using salted sha512 for htpasswd by default

This commit is contained in:
Maxim Devaev 2025-02-11 16:55:45 +02:00
parent 800d2724b8
commit de4f1903aa
8 changed files with 88 additions and 29 deletions

View File

@ -1 +1 @@
admin:$apr1$.6mu9N8n$xOuGesr4JZZkdiZo/j318. admin:{SSHA512}3zSmw/L9zIkpQdX5bcy6HntTxltAzTuGNP6NjHRRgOcNZkA0K+Lsrj3QplO9Gr3BA5MYVVki9rAVnFNCcIdtYC6FkLJWCmHs

View File

@ -30,14 +30,14 @@ import argparse
from typing import Generator from typing import Generator
import passlib.apache
from ...yamlconf import Section from ...yamlconf import Section
from ...validators import ValidatorError from ...validators import ValidatorError
from ...validators.auth import valid_user from ...validators.auth import valid_user
from ...validators.auth import valid_passwd from ...validators.auth import valid_passwd
from ...crypto import KvmdHtpasswdFile
from .. import init from .. import init
@ -50,7 +50,7 @@ def _get_htpasswd_path(config: Section) -> str:
@contextlib.contextmanager @contextlib.contextmanager
def _get_htpasswd_for_write(config: Section) -> Generator[passlib.apache.HtpasswdFile, None, None]: def _get_htpasswd_for_write(config: Section) -> Generator[KvmdHtpasswdFile, None, None]:
path = _get_htpasswd_path(config) path = _get_htpasswd_path(config)
(tmp_fd, tmp_path) = tempfile.mkstemp( (tmp_fd, tmp_path) = tempfile.mkstemp(
prefix=f".{os.path.basename(path)}.", prefix=f".{os.path.basename(path)}.",
@ -65,7 +65,7 @@ def _get_htpasswd_for_write(config: Section) -> Generator[passlib.apache.Htpassw
os.fchmod(tmp_fd, st.st_mode) os.fchmod(tmp_fd, st.st_mode)
finally: finally:
os.close(tmp_fd) os.close(tmp_fd)
htpasswd = passlib.apache.HtpasswdFile(tmp_path) htpasswd = KvmdHtpasswdFile(tmp_path)
yield htpasswd yield htpasswd
htpasswd.save() htpasswd.save()
os.rename(tmp_path, path) os.rename(tmp_path, path)
@ -96,7 +96,7 @@ def _print_invalidate_tip(prepend_nl: bool) -> None:
# ==== # ====
def _cmd_list(config: Section, _: argparse.Namespace) -> None: def _cmd_list(config: Section, _: argparse.Namespace) -> None:
for user in sorted(passlib.apache.HtpasswdFile(_get_htpasswd_path(config)).users()): for user in sorted(KvmdHtpasswdFile(_get_htpasswd_path(config)).users()):
print(user) print(user)

58
kvmd/crypto.py Normal file
View File

@ -0,0 +1,58 @@
# ========================================================================== #
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
# ========================================================================== #
from passlib.context import CryptContext
from passlib.apache import HtpasswdFile as _ApacheHtpasswdFile
from passlib.apache import htpasswd_context as _apache_htpasswd_ctx
# =====
_SHA512 = "ldap_salted_sha512"
_SHA256 = "ldap_salted_sha256"
def _make_kvmd_htpasswd_context() -> CryptContext:
schemes = list(_apache_htpasswd_ctx.schemes())
for alg in [_SHA256, _SHA512]:
if alg in schemes:
schemes.remove(alg)
schemes.insert(0, alg)
assert schemes[0] == _SHA512
return CryptContext(
schemes=schemes,
default=_SHA512,
bcrypt__ident="2y", # See note in the passlib.apache
)
_kvmd_htpasswd_ctx = _make_kvmd_htpasswd_context()
# =====
class KvmdHtpasswdFile(_ApacheHtpasswdFile):
def __init__(self, path: str, new: bool=False) -> None:
super().__init__(
path=path,
default_scheme=_SHA512,
context=_kvmd_htpasswd_ctx,
new=new,
)

View File

@ -20,12 +20,12 @@
# ========================================================================== # # ========================================================================== #
import passlib.apache
from ...yamlconf import Option from ...yamlconf import Option
from ...validators.os import valid_abs_file from ...validators.os import valid_abs_file
from ...crypto import KvmdHtpasswdFile
from . import BaseAuthService from . import BaseAuthService
@ -43,5 +43,5 @@ class Plugin(BaseAuthService):
async def authorize(self, user: str, passwd: str) -> bool: async def authorize(self, user: str, passwd: str) -> bool:
assert user == user.strip() assert user == user.strip()
assert user assert user
htpasswd = passlib.apache.HtpasswdFile(self.__path) htpasswd = KvmdHtpasswdFile(self.__path)
return htpasswd.check_password(user, passwd) return htpasswd.check_password(user, passwd)

View File

@ -46,6 +46,7 @@ RUN pacman --noconfirm --ask=4 -Syy \
python-aiofiles \ python-aiofiles \
python-async-lru \ python-async-lru \
python-passlib \ python-passlib \
python-bcrypt \
python-pyotp \ python-pyotp \
python-qrcode \ python-qrcode \
python-pyserial \ python-pyserial \

View File

@ -29,12 +29,12 @@ import getpass
from typing import Generator from typing import Generator
from typing import Any from typing import Any
import passlib.apache
import pytest import pytest
from kvmd.apps.htpasswd import main from kvmd.apps.htpasswd import main
from kvmd.crypto import KvmdHtpasswdFile
# ===== # =====
def _make_passwd(user: str) -> str: def _make_passwd(user: str) -> str:
@ -42,10 +42,10 @@ def _make_passwd(user: str) -> str:
@pytest.fixture(name="htpasswd", params=[[], ["admin"], ["admin", "user"]]) @pytest.fixture(name="htpasswd", params=[[], ["admin"], ["admin", "user"]])
def _htpasswd_fixture(request) -> Generator[passlib.apache.HtpasswdFile, None, None]: # type: ignore def _htpasswd_fixture(request) -> Generator[KvmdHtpasswdFile, None, None]: # type: ignore
(fd, path) = tempfile.mkstemp() (fd, path) = tempfile.mkstemp()
os.close(fd) os.close(fd)
htpasswd = passlib.apache.HtpasswdFile(path) htpasswd = KvmdHtpasswdFile(path)
for user in request.param: for user in request.param:
htpasswd.set_password(user, _make_passwd(user)) htpasswd.set_password(user, _make_passwd(user))
htpasswd.save() htpasswd.save()
@ -63,7 +63,7 @@ def _run_htpasswd(cmd: list[str], htpasswd_path: str, int_type: str="htpasswd")
# ===== # =====
def test_ok__list(htpasswd: passlib.apache.HtpasswdFile, capsys) -> None: # type: ignore def test_ok__list(htpasswd: KvmdHtpasswdFile, capsys) -> None: # type: ignore
_run_htpasswd(["list"], htpasswd.path) _run_htpasswd(["list"], htpasswd.path)
(out, err) = capsys.readouterr() (out, err) = capsys.readouterr()
assert len(err) == 0 assert len(err) == 0
@ -71,7 +71,7 @@ def test_ok__list(htpasswd: passlib.apache.HtpasswdFile, capsys) -> None: # typ
# ===== # =====
def test_ok__set_change_stdin(htpasswd: passlib.apache.HtpasswdFile, mocker) -> None: # type: ignore def test_ok__set_change_stdin(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore
old_users = set(htpasswd.users()) old_users = set(htpasswd.users())
if old_users: if old_users:
assert htpasswd.check_password("admin", _make_passwd("admin")) assert htpasswd.check_password("admin", _make_passwd("admin"))
@ -84,7 +84,7 @@ def test_ok__set_change_stdin(htpasswd: passlib.apache.HtpasswdFile, mocker) ->
assert old_users == set(htpasswd.users()) assert old_users == set(htpasswd.users())
def test_ok__set_add_stdin(htpasswd: passlib.apache.HtpasswdFile, mocker) -> None: # type: ignore def test_ok__set_add_stdin(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore
old_users = set(htpasswd.users()) old_users = set(htpasswd.users())
if old_users: if old_users:
mocker.patch.object(builtins, "input", (lambda: " test ")) mocker.patch.object(builtins, "input", (lambda: " test "))
@ -96,7 +96,7 @@ def test_ok__set_add_stdin(htpasswd: passlib.apache.HtpasswdFile, mocker) -> Non
# ===== # =====
def test_ok__set_change_getpass(htpasswd: passlib.apache.HtpasswdFile, mocker) -> None: # type: ignore def test_ok__set_change_getpass(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore
old_users = set(htpasswd.users()) old_users = set(htpasswd.users())
if old_users: if old_users:
assert htpasswd.check_password("admin", _make_passwd("admin")) assert htpasswd.check_password("admin", _make_passwd("admin"))
@ -109,7 +109,7 @@ def test_ok__set_change_getpass(htpasswd: passlib.apache.HtpasswdFile, mocker) -
assert old_users == set(htpasswd.users()) assert old_users == set(htpasswd.users())
def test_fail__set_change_getpass(htpasswd: passlib.apache.HtpasswdFile, mocker) -> None: # type: ignore def test_fail__set_change_getpass(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore
old_users = set(htpasswd.users()) old_users = set(htpasswd.users())
if old_users: if old_users:
assert htpasswd.check_password("admin", _make_passwd("admin")) assert htpasswd.check_password("admin", _make_passwd("admin"))
@ -137,7 +137,7 @@ def test_fail__set_change_getpass(htpasswd: passlib.apache.HtpasswdFile, mocker)
# ===== # =====
def test_ok__del(htpasswd: passlib.apache.HtpasswdFile) -> None: def test_ok__del(htpasswd: KvmdHtpasswdFile) -> None:
old_users = set(htpasswd.users()) old_users = set(htpasswd.users())
if old_users: if old_users:

View File

@ -26,8 +26,6 @@ import contextlib
from typing import AsyncGenerator from typing import AsyncGenerator
import passlib.apache
import pytest import pytest
from kvmd.yamlconf import make_config from kvmd.yamlconf import make_config
@ -38,6 +36,8 @@ from kvmd.plugins.auth import get_auth_service_class
from kvmd.htserver import HttpExposed from kvmd.htserver import HttpExposed
from kvmd.crypto import KvmdHtpasswdFile
# ===== # =====
_E_AUTH = HttpExposed("GET", "/foo_auth", True, (lambda: None)) _E_AUTH = HttpExposed("GET", "/foo_auth", True, (lambda: None))
@ -85,7 +85,7 @@ async def _get_configured_manager(
async def test_ok__expire(tmpdir) -> None: # type: ignore async def test_ok__expire(tmpdir) -> None: # type: ignore
path = os.path.abspath(str(tmpdir.join("htpasswd"))) path = os.path.abspath(str(tmpdir.join("htpasswd")))
htpasswd = passlib.apache.HtpasswdFile(path, new=True) htpasswd = KvmdHtpasswdFile(path, new=True)
htpasswd.set_password("admin", "pass") htpasswd.set_password("admin", "pass")
htpasswd.save() htpasswd.save()
@ -153,7 +153,7 @@ async def test_ok__expire(tmpdir) -> None: # type: ignore
async def test_ok__internal(tmpdir) -> None: # type: ignore async def test_ok__internal(tmpdir) -> None: # type: ignore
path = os.path.abspath(str(tmpdir.join("htpasswd"))) path = os.path.abspath(str(tmpdir.join("htpasswd")))
htpasswd = passlib.apache.HtpasswdFile(path, new=True) htpasswd = KvmdHtpasswdFile(path, new=True)
htpasswd.set_password("admin", "pass") htpasswd.set_password("admin", "pass")
htpasswd.save() htpasswd.save()
@ -201,12 +201,12 @@ async def test_ok__external(tmpdir) -> None: # type: ignore
path1 = os.path.abspath(str(tmpdir.join("htpasswd1"))) path1 = os.path.abspath(str(tmpdir.join("htpasswd1")))
path2 = os.path.abspath(str(tmpdir.join("htpasswd2"))) path2 = os.path.abspath(str(tmpdir.join("htpasswd2")))
htpasswd1 = passlib.apache.HtpasswdFile(path1, new=True) htpasswd1 = KvmdHtpasswdFile(path1, new=True)
htpasswd1.set_password("admin", "pass1") htpasswd1.set_password("admin", "pass1")
htpasswd1.set_password("local", "foobar") htpasswd1.set_password("local", "foobar")
htpasswd1.save() htpasswd1.save()
htpasswd2 = passlib.apache.HtpasswdFile(path2, new=True) htpasswd2 = KvmdHtpasswdFile(path2, new=True)
htpasswd2.set_password("admin", "pass2") htpasswd2.set_password("admin", "pass2")
htpasswd2.set_password("user", "foobar") htpasswd2.set_password("user", "foobar")
htpasswd2.save() htpasswd2.save()
@ -239,7 +239,7 @@ async def test_ok__external(tmpdir) -> None: # type: ignore
async def test_ok__unauth(tmpdir) -> None: # type: ignore async def test_ok__unauth(tmpdir) -> None: # type: ignore
path = os.path.abspath(str(tmpdir.join("htpasswd"))) path = os.path.abspath(str(tmpdir.join("htpasswd")))
htpasswd = passlib.apache.HtpasswdFile(path, new=True) htpasswd = KvmdHtpasswdFile(path, new=True)
htpasswd.set_password("admin", "pass") htpasswd.set_password("admin", "pass")
htpasswd.save() htpasswd.save()

View File

@ -22,10 +22,10 @@
import os import os
import passlib.apache
import pytest import pytest
from kvmd.crypto import KvmdHtpasswdFile
from . import get_configured_auth_service from . import get_configured_auth_service
@ -34,7 +34,7 @@ from . import get_configured_auth_service
async def test_ok__htpasswd_service(tmpdir) -> None: # type: ignore async def test_ok__htpasswd_service(tmpdir) -> None: # type: ignore
path = os.path.abspath(str(tmpdir.join("htpasswd"))) path = os.path.abspath(str(tmpdir.join("htpasswd")))
htpasswd = passlib.apache.HtpasswdFile(path, new=True) htpasswd = KvmdHtpasswdFile(path, new=True)
htpasswd.set_password("admin", "pass") htpasswd.set_password("admin", "pass")
htpasswd.save() htpasswd.save()