From de4f1903aa2de7affca5d027b2ac38fd51354c93 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 11 Feb 2025 16:55:45 +0200 Subject: [PATCH] using salted sha512 for htpasswd by default --- configs/kvmd/htpasswd | 2 +- kvmd/apps/htpasswd/__init__.py | 10 ++-- kvmd/crypto.py | 58 +++++++++++++++++++++ kvmd/plugins/auth/htpasswd.py | 6 +-- testenv/Dockerfile | 1 + testenv/tests/apps/htpasswd/test_main.py | 20 +++---- testenv/tests/apps/kvmd/test_auth.py | 14 ++--- testenv/tests/plugins/auth/test_htpasswd.py | 6 +-- 8 files changed, 88 insertions(+), 29 deletions(-) create mode 100644 kvmd/crypto.py diff --git a/configs/kvmd/htpasswd b/configs/kvmd/htpasswd index a6cbfca9..fce6127b 100644 --- a/configs/kvmd/htpasswd +++ b/configs/kvmd/htpasswd @@ -1 +1 @@ -admin:$apr1$.6mu9N8n$xOuGesr4JZZkdiZo/j318. +admin:{SSHA512}3zSmw/L9zIkpQdX5bcy6HntTxltAzTuGNP6NjHRRgOcNZkA0K+Lsrj3QplO9Gr3BA5MYVVki9rAVnFNCcIdtYC6FkLJWCmHs diff --git a/kvmd/apps/htpasswd/__init__.py b/kvmd/apps/htpasswd/__init__.py index 9e857abc..4669f6c0 100644 --- a/kvmd/apps/htpasswd/__init__.py +++ b/kvmd/apps/htpasswd/__init__.py @@ -30,14 +30,14 @@ import argparse from typing import Generator -import passlib.apache - from ...yamlconf import Section from ...validators import ValidatorError from ...validators.auth import valid_user from ...validators.auth import valid_passwd +from ...crypto import KvmdHtpasswdFile + from .. import init @@ -50,7 +50,7 @@ def _get_htpasswd_path(config: Section) -> str: @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) (tmp_fd, tmp_path) = tempfile.mkstemp( 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) finally: os.close(tmp_fd) - htpasswd = passlib.apache.HtpasswdFile(tmp_path) + htpasswd = KvmdHtpasswdFile(tmp_path) yield htpasswd htpasswd.save() 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: - for user in sorted(passlib.apache.HtpasswdFile(_get_htpasswd_path(config)).users()): + for user in sorted(KvmdHtpasswdFile(_get_htpasswd_path(config)).users()): print(user) diff --git a/kvmd/crypto.py b/kvmd/crypto.py new file mode 100644 index 00000000..855c3c18 --- /dev/null +++ b/kvmd/crypto.py @@ -0,0 +1,58 @@ +# ========================================================================== # +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2024 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + + +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, + ) diff --git a/kvmd/plugins/auth/htpasswd.py b/kvmd/plugins/auth/htpasswd.py index 64fe2d3f..1c54060a 100644 --- a/kvmd/plugins/auth/htpasswd.py +++ b/kvmd/plugins/auth/htpasswd.py @@ -20,12 +20,12 @@ # ========================================================================== # -import passlib.apache - from ...yamlconf import Option from ...validators.os import valid_abs_file +from ...crypto import KvmdHtpasswdFile + from . import BaseAuthService @@ -43,5 +43,5 @@ class Plugin(BaseAuthService): async def authorize(self, user: str, passwd: str) -> bool: assert user == user.strip() assert user - htpasswd = passlib.apache.HtpasswdFile(self.__path) + htpasswd = KvmdHtpasswdFile(self.__path) return htpasswd.check_password(user, passwd) diff --git a/testenv/Dockerfile b/testenv/Dockerfile index f907bb3d..bb7c7bd7 100644 --- a/testenv/Dockerfile +++ b/testenv/Dockerfile @@ -46,6 +46,7 @@ RUN pacman --noconfirm --ask=4 -Syy \ python-aiofiles \ python-async-lru \ python-passlib \ + python-bcrypt \ python-pyotp \ python-qrcode \ python-pyserial \ diff --git a/testenv/tests/apps/htpasswd/test_main.py b/testenv/tests/apps/htpasswd/test_main.py index 4d7173f0..d66a887c 100644 --- a/testenv/tests/apps/htpasswd/test_main.py +++ b/testenv/tests/apps/htpasswd/test_main.py @@ -29,12 +29,12 @@ import getpass from typing import Generator from typing import Any -import passlib.apache - import pytest from kvmd.apps.htpasswd import main +from kvmd.crypto import KvmdHtpasswdFile + # ===== def _make_passwd(user: str) -> str: @@ -42,10 +42,10 @@ def _make_passwd(user: str) -> str: @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() os.close(fd) - htpasswd = passlib.apache.HtpasswdFile(path) + htpasswd = KvmdHtpasswdFile(path) for user in request.param: htpasswd.set_password(user, _make_passwd(user)) 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) (out, err) = capsys.readouterr() 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()) if old_users: 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()) -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()) if old_users: 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()) if old_users: 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()) -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()) if old_users: 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()) if old_users: diff --git a/testenv/tests/apps/kvmd/test_auth.py b/testenv/tests/apps/kvmd/test_auth.py index 39175156..de53fc32 100644 --- a/testenv/tests/apps/kvmd/test_auth.py +++ b/testenv/tests/apps/kvmd/test_auth.py @@ -26,8 +26,6 @@ import contextlib from typing import AsyncGenerator -import passlib.apache - import pytest 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.crypto import KvmdHtpasswdFile + # ===== _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 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.save() @@ -153,7 +153,7 @@ async def test_ok__expire(tmpdir) -> None: # type: ignore async def test_ok__internal(tmpdir) -> None: # type: ignore 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.save() @@ -201,12 +201,12 @@ async def test_ok__external(tmpdir) -> None: # type: ignore path1 = os.path.abspath(str(tmpdir.join("htpasswd1"))) 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("local", "foobar") htpasswd1.save() - htpasswd2 = passlib.apache.HtpasswdFile(path2, new=True) + htpasswd2 = KvmdHtpasswdFile(path2, new=True) htpasswd2.set_password("admin", "pass2") htpasswd2.set_password("user", "foobar") htpasswd2.save() @@ -239,7 +239,7 @@ async def test_ok__external(tmpdir) -> None: # type: ignore async def test_ok__unauth(tmpdir) -> None: # type: ignore 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.save() diff --git a/testenv/tests/plugins/auth/test_htpasswd.py b/testenv/tests/plugins/auth/test_htpasswd.py index 12d40b23..398f05d3 100644 --- a/testenv/tests/plugins/auth/test_htpasswd.py +++ b/testenv/tests/plugins/auth/test_htpasswd.py @@ -22,10 +22,10 @@ import os -import passlib.apache - import pytest +from kvmd.crypto import KvmdHtpasswdFile + 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 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.save()