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.
This commit is contained in:
Thomas Waldmann 2026-05-11 20:15:17 +02:00
parent d3a9799563
commit 710db85b20
No known key found for this signature in database
GPG key ID: 243ACFA951F78E01
9 changed files with 91 additions and 21 deletions

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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",

View file

@ -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}

View file

@ -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)

View file

@ -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(

View file

@ -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):

View file

@ -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, &parallelism)
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, <void *>salt_c, len(salt))
p[5] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, <void *>secret_c, len(secret))
p[6] = OSSL_PARAM_construct_end()
result = PyBytes_FromStringAndSize(NULL, hash_len)
if EVP_KDF_derive(kctx, <unsigned char *>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