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 ca3e88f5b1
commit 9fa76bc437
No known key found for this signature in database
GPG key ID: 243ACFA951F78E01
6 changed files with 85 additions and 18 deletions

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

@ -241,7 +241,7 @@ on widely used libraries providing them:
primitives implemented in libcrypto.
- SHA-256, SHA-512 and BLAKE2b from Python's hashlib_ standard library module are used.
- 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.
"xxhash>=2.0.0",

View file

@ -139,7 +139,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
@ -151,7 +151,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

@ -11,8 +11,6 @@ from ..logger import create_logger
logger = create_logger()
import argon2.low_level
from ..constants import * # NOQA
from ..helpers import StableDict
from ..helpers import Error, IntegrityError
@ -466,17 +464,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_pbkdf2(self, encrypted_key, passphrase):
key = self.pbkdf2(passphrase, encrypted_key.salt, encrypted_key.iterations, 32)

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
@ -944,3 +972,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