From 710db85b20e20c8b21c4b3f6777a2efe05efccd9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 11 May 2026 20:15:17 +0200 Subject: [PATCH] use argon2 from openssl >= 3.2, drop argon2-cffi, fixes #7963 - src/borg/crypto/low_level.pyx: implement `argon2_hash` using OpenSSL's `EVP_KDF` API for ARGON2 (requires OpenSSL >= 3.2.0). - src/borg/crypto/key.py: switch to the native `argon2_hash` implementation, removing `argon2-cffi` dependency. - setup.py: require OpenSSL >= 3.2.0 for the crypto extension to ensure ARGON2 KDF support is available. - pyproject.toml: drop `argon2-cffi` dependency. - docs: update installation requirements and security documentation to reflect the transition to OpenSSL for Argon2. --- .github/workflows/ci.yml | 5 +-- docs/installation.rst | 4 +- docs/internals/security.rst | 2 +- pyproject.toml | 1 - scripts/msys2-install-deps | 2 +- setup.py | 4 +- src/borg/crypto/key.py | 13 +----- src/borg/crypto/low_level.pyi | 3 ++ src/borg/crypto/low_level.pyx | 78 +++++++++++++++++++++++++++++++++++ 9 files changed, 91 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a0e78406..d57bf2038 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -568,8 +568,7 @@ jobs: pkgman install -y openssl3 pkgman install -y rust_bin pkgman install -y python3.11 - pkgman install -y cffi - pkgman install -y lz4_devel openssl3_devel libffi_devel + pkgman install -y lz4_devel openssl3_devel # there is no pkgman package for tox, so we install it into a venv python3 -m ensurepip --upgrade @@ -655,7 +654,7 @@ jobs: - name: Build python venv run: | - # building cffi / argon2-cffi in the venv fails, so we try to use the system packages + # building native extensions in the venv fails, so we try to use the system packages python -m venv --system-site-packages env . env/bin/activate # python -m pip install --upgrade pip diff --git a/docs/installation.rst b/docs/installation.rst index 62c326098..8b9de4d1f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -162,10 +162,10 @@ following dependencies first. For the libraries you will also need their development header files (sometimes in a separate `-dev` or `-devel` package). * `Python 3`_ >= 3.11.0 -* OpenSSL_ >= 1.1.1 (LibreSSL will not work) +* OpenSSL_ >= 3.2.0 (LibreSSL will not work) * libacl_ (which depends on libattr_) * liblz4_ >= 1.7.0 (r129) -* libffi (required for argon2-cffi-bindings) + * pkg-config (cli tool) - Borg uses this to discover header and library locations automatically. Alternatively, you can also point to them via some environment variables, see setup.py. diff --git a/docs/internals/security.rst b/docs/internals/security.rst index 1534988ae..727376ce3 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -268,7 +268,7 @@ on widely used libraries providing them: - BLAKE3 is used via the blake3_ package (new repos, ``--id-hash blake3``). - BLAKE2b from Python's hashlib_ is only used to read legacy (borg < 2.0) repos. - HMAC and a constant-time comparison from Python's hmac_ standard library module are used. -- argon2 is used via argon2-cffi. +- argon2 is used from OpenSSL (>= 3.2). .. _Horton principle: https://en.wikipedia.org/wiki/Horton_Principle .. _length extension: https://en.wikipedia.org/wiki/Length_extension_attack diff --git a/pyproject.toml b/pyproject.toml index f5965623a..a3943ac88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ dependencies = [ "packaging", "platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin'", # for macOS: breaking changes in 3.0.0. "platformdirs >=2.6.0, <5.0.0; sys_platform != 'darwin'", # for others: 2.6+ works consistently. - "argon2-cffi", "shtab>=1.8.0", "backports-zstd; python_version < '3.14'", # for python < 3.14. "jsonargparse>=4.47.0", diff --git a/scripts/msys2-install-deps b/scripts/msys2-install-deps index ae4dc4bd8..0cbf86424 100644 --- a/scripts/msys2-install-deps +++ b/scripts/msys2-install-deps @@ -1,6 +1,6 @@ #!/bin/bash -pacman -S --needed --noconfirm git mingw-w64-ucrt-x86_64-{toolchain,pkgconf,lz4,openssl,rclone,python-msgpack,python-argon2_cffi,python-platformdirs,python,cython,python-setuptools,python-wheel,python-build,python-pkgconfig,python-packaging,python-pip,python-paramiko,rust,python-maturin} +pacman -S --needed --noconfirm git mingw-w64-ucrt-x86_64-{toolchain,pkgconf,lz4,openssl,rclone,python-msgpack,python-platformdirs,python,cython,python-setuptools,python-wheel,python-build,python-pkgconfig,python-packaging,python-pip,python-paramiko,rust,python-maturin} if [ "$1" = "development" ]; then pacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-python-{pytest,pytest-benchmark,pytest-cov,pytest-xdist} diff --git a/setup.py b/setup.py index 1ad76e804..04d34a00b 100644 --- a/setup.py +++ b/setup.py @@ -144,7 +144,7 @@ if not on_rtd: ) if is_win32: - crypto_ext_lib = lib_ext_kwargs(pc, "BORG_OPENSSL_PREFIX", "libcrypto", "libcrypto", ">=1.1.1", lib_subdir="") + crypto_ext_lib = lib_ext_kwargs(pc, "BORG_OPENSSL_PREFIX", "libcrypto", "libcrypto", ">=3.2.0", lib_subdir="") elif is_openbsd: # Use OpenSSL (not LibreSSL) because we need AES-OCB via the EVP API. Link # it statically to avoid conflicting with shared libcrypto from the base @@ -156,7 +156,7 @@ if not on_rtd: extra_objects=[os.path.join(openssl_prefix, "lib", openssl_name, "libcrypto.a")], ) else: - crypto_ext_lib = lib_ext_kwargs(pc, "BORG_OPENSSL_PREFIX", "crypto", "libcrypto", ">=1.1.1") + crypto_ext_lib = lib_ext_kwargs(pc, "BORG_OPENSSL_PREFIX", "crypto", "libcrypto", ">=3.2.0") crypto_ext_kwargs = members_appended( dict(sources=[crypto_ll_source]), crypto_ext_lib, dict(extra_compile_args=cflags) diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 60d66d168..8c342e325 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -12,7 +12,6 @@ from ..logger import create_logger logger = create_logger() from blake3 import blake3 -import argon2.low_level from ..constants import * # NOQA from ..helpers import StableDict @@ -508,17 +507,9 @@ class FlexiKey: parallelism = 1 # 8 is the smallest value that avoids the "Memory cost is too small" exception memory_cost = 8 - type_map = {"i": argon2.low_level.Type.I, "d": argon2.low_level.Type.D, "id": argon2.low_level.Type.ID} - key = argon2.low_level.hash_secret_raw( - secret=passphrase.encode("utf-8"), - hash_len=output_len_in_bytes, - salt=salt, - time_cost=time_cost, - memory_cost=memory_cost, - parallelism=parallelism, - type=type_map[type], + return low_level.argon2_hash( + passphrase.encode("utf-8"), salt, time_cost, memory_cost, parallelism, output_len_in_bytes, type ) - return key def decrypt_key_file_argon2(self, encrypted_key, passphrase): key = self.argon2( diff --git a/src/borg/crypto/low_level.pyi b/src/borg/crypto/low_level.pyi index 9d5e4c056..07922a228 100644 --- a/src/borg/crypto/low_level.pyi +++ b/src/borg/crypto/low_level.pyi @@ -12,6 +12,9 @@ def long_to_bytes(x: int) -> bytes: ... def hmac_sha256(key: bytes, data: bytes) -> bytes: ... def blake2b_256(key: bytes, data: bytes) -> bytes: ... def blake2b_128(data: bytes) -> bytes: ... +def argon2_hash( + secret: bytes, salt: bytes, time_cost: int, memory_cost: int, parallelism: int, hash_len: int, type: str +) -> bytes: ... # Exception classes class CryptoError(Exception): diff --git a/src/borg/crypto/low_level.pyx b/src/borg/crypto/low_level.pyx index 149ea1d73..ca824a948 100644 --- a/src/borg/crypto/low_level.pyx +++ b/src/borg/crypto/low_level.pyx @@ -88,6 +88,34 @@ cdef extern from "openssl/evp.h": int EVP_CTRL_AEAD_SET_TAG int EVP_CTRL_AEAD_SET_IVLEN +cdef extern from "openssl/kdf.h": + ctypedef struct EVP_KDF: + pass + ctypedef struct EVP_KDF_CTX: + pass + EVP_KDF *EVP_KDF_fetch(void *ctx, const char *algorithm, const char *properties) + void EVP_KDF_free(EVP_KDF *kdf) + EVP_KDF_CTX *EVP_KDF_CTX_new(EVP_KDF *kdf) + void EVP_KDF_CTX_free(EVP_KDF_CTX *ctx) + +cdef extern from "openssl/params.h": + ctypedef struct OSSL_PARAM: + pass + OSSL_PARAM OSSL_PARAM_construct_uint32(const char *key, uint32_t *buf) + OSSL_PARAM OSSL_PARAM_construct_octet_string(const char *key, void *buf, size_t bsize) + OSSL_PARAM OSSL_PARAM_construct_end() + +cdef extern from "openssl/core_names.h": + const char *OSSL_KDF_PARAM_THREADS + const char *OSSL_KDF_PARAM_ARGON2_LANES + const char *OSSL_KDF_PARAM_ITER + const char *OSSL_KDF_PARAM_ARGON2_MEMCOST + const char *OSSL_KDF_PARAM_SALT + const char *OSSL_KDF_PARAM_PASSWORD + +cdef extern from "openssl/kdf.h": + int EVP_KDF_derive(EVP_KDF_CTX *ctx, unsigned char *key, size_t keylen, const OSSL_PARAM params[]) + import struct @@ -825,3 +853,53 @@ cdef class CSPRNG: # Swap items[i] and items[j] items[i], items[j] = items[j], items[i] + + +def argon2_hash(bytes secret, bytes salt, uint32_t time_cost, uint32_t memory_cost, + uint32_t parallelism, uint32_t hash_len, type): + cdef EVP_KDF *kdf = NULL + cdef EVP_KDF_CTX *kctx = NULL + cdef OSSL_PARAM params[8] + cdef OSSL_PARAM *p = params + cdef uint32_t threads = 1 + cdef bytes result + cdef const char *alg_name + cdef const unsigned char *secret_c = secret + cdef const unsigned char *salt_c = salt + + if type == "i" or type == b"i": + alg_name = b"ARGON2I" + elif type == "d" or type == b"d": + alg_name = b"ARGON2D" + elif type == "id" or type == b"id": + alg_name = b"ARGON2ID" + else: + raise ValueError("Invalid argon2 type") + + kdf = EVP_KDF_fetch(NULL, alg_name, NULL) + if kdf == NULL: + raise CryptoError("Argon2 KDF not found in OpenSSL (requires >= 3.2)") + + kctx = EVP_KDF_CTX_new(kdf) + if kctx == NULL: + EVP_KDF_free(kdf) + raise MemoryError("Failed to create KDF context") + + p[0] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &threads) + p[1] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, ¶llelism) + p[2] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, &time_cost) + p[3] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &memory_cost) + p[4] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, salt_c, len(salt)) + p[5] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, secret_c, len(secret)) + p[6] = OSSL_PARAM_construct_end() + + result = PyBytes_FromStringAndSize(NULL, hash_len) + + if EVP_KDF_derive(kctx, result, hash_len, params) <= 0: + EVP_KDF_CTX_free(kctx) + EVP_KDF_free(kdf) + raise CryptoError("EVP_KDF_derive failed") + + EVP_KDF_CTX_free(kctx) + EVP_KDF_free(kdf) + return result