diff --git a/data.py-dist b/data.py-dist index 3b3df5e10..b8544a7fd 100644 --- a/data.py-dist +++ b/data.py-dist @@ -4,7 +4,7 @@ from __future__ import annotations import os -import legacycrypt as crypt # type: ignore[import-untyped] +from lib.common import hash_password from typing import TYPE_CHECKING, Any @@ -17,11 +17,6 @@ if TYPE_CHECKING: HOST_DEFAULT_USER = "root" HOST_DEFAULT_PASSWORD = "" -def hash_password(password): - """Hash password for /etc/password.""" - salt = crypt.mksalt(crypt.METHOD_SHA512) # type: ignore - return crypt.crypt(password, salt) - HOST_DEFAULT_PASSWORD_HASH = hash_password(HOST_DEFAULT_PASSWORD) # Public keys for a private keys available to the test runner diff --git a/lib/common.py b/lib/common.py index 58cf612c8..b94459d1c 100644 --- a/lib/common.py +++ b/lib/common.py @@ -18,6 +18,7 @@ from uuid import UUID import requests +from passlib.hash import sha512_crypt from pydantic import TypeAdapter from typing import ( @@ -398,3 +399,8 @@ def _param_clear(host: Host, xe_prefix: str, uuid: str, param_name: str) -> None """ Common implementation for param_clear. """ args: dict[str, str | bool | dict[str, str]] = {'uuid': uuid, 'param-name': param_name} host.xe(f'{xe_prefix}-param-clear', args) + +def hash_password(password: str) -> str: + """Hash password for /etc/shadow.""" + # XCP-ng uses sha512 with 5000 rounds by default + return sha512_crypt.using(rounds=5000).hash(password) diff --git a/pyproject.toml b/pyproject.toml index 257fa59c9..21dfaa24b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,10 @@ requires-python = ">=3.11" dependencies = [ "cryptography>=3.3.1", "gitpython", + # TODO: keep legacycrypt to let the user transition to lib.common.hash_password in their data.py "legacycrypt", "packaging>=20.7", + "passlib", "pluggy>=1.1.0", "pydantic", "pytest>=8.0.0", @@ -34,6 +36,7 @@ dev = [ "zizmor", "prek>=0.4.1", "autopep8", + "types-passlib", ] [tool.pyright] diff --git a/requirements/base.txt b/requirements/base.txt index 5992b12f6..8162a2363 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -3,6 +3,7 @@ cryptography>=3.3.1 gitpython legacycrypt packaging>=20.7 +passlib pluggy>=1.1.0 pydantic pytest>=8.0.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index 6e3cb3f0e..d3d622f7f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -15,4 +15,5 @@ types-pexpect zizmor prek>=0.4.1 autopep8 +types-passlib -r base.txt diff --git a/uv.lock b/uv.lock index 144ed463c..52276249b 100644 --- a/uv.lock +++ b/uv.lock @@ -532,6 +532,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, +] + [[package]] name = "pathspec" version = "1.0.4" @@ -867,6 +876,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/01/61ac9eb38f1f978b47443dc6fd2e0a3b0f647c2da741ddad30771f1b2b6f/types_docutils-0.22.3.20251115-py3-none-any.whl", hash = "sha256:c6e53715b65395d00a75a3a8a74e352c669bc63959e65a207dffaa22f4a2ad6e", size = 91951, upload-time = "2025-11-15T02:59:56.413Z" }, ] +[[package]] +name = "types-passlib" +version = "1.7.7.20260211" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/718ff8cbef9366e597aefd58929321702d3e183998cea89949ab5423281f/types_passlib-1.7.7.20260211.tar.gz", hash = "sha256:af73afffe1ce94c95c7f6072bd261572c29845de74fdffa3a265fc7634bca056", size = 25666, upload-time = "2026-02-10T15:11:59.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/6a/e9fc6a5b8f9a380a4a56b9f1e4dba5c6899561868017b17f6de382808b6f/types_passlib-1.7.7.20260211-py3-none-any.whl", hash = "sha256:c0f1ad440c513a6c07f333b28249530686056fd54a7b3ac6128ae31fd46305d3", size = 40457, upload-time = "2026-02-10T15:11:58.647Z" }, +] + [[package]] name = "types-pexpect" version = "4.9.0.20260518" @@ -939,6 +957,7 @@ dependencies = [ { name = "gitpython" }, { name = "legacycrypt" }, { name = "packaging" }, + { name = "passlib" }, { name = "pluggy" }, { name = "pydantic" }, { name = "pytest" }, @@ -959,6 +978,7 @@ dev = [ { name = "pyright" }, { name = "ruff" }, { name = "types-colorama" }, + { name = "types-passlib" }, { name = "types-pexpect" }, { name = "types-pygments" }, { name = "types-requests" }, @@ -972,6 +992,7 @@ requires-dist = [ { name = "gitpython" }, { name = "legacycrypt" }, { name = "packaging", specifier = ">=20.7" }, + { name = "passlib" }, { name = "pluggy", specifier = ">=1.1.0" }, { name = "pydantic" }, { name = "pytest", specifier = ">=8.0.0" }, @@ -992,6 +1013,7 @@ dev = [ { name = "pyright" }, { name = "ruff" }, { name = "types-colorama" }, + { name = "types-passlib" }, { name = "types-pexpect" }, { name = "types-pygments" }, { name = "types-requests" },