mirror of
https://github.com/borgbackup/borg.git
synced 2026-10-12 02:11:36 +00:00
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. The lanes are computed in parallel via OpenSSL's thread pool: OpenSSL's Argon2 only uses threads if OSSL_set_max_threads() enabled the thread pool and a "threads" parameter > 1 is given. Passing threads=1 would compute the 4 lanes of our default parallelism=4 sequentially, and unlocking would get slower than it was with argon2-cffi, which used real threads for the lanes. The thread count only affects speed, never the derived key (only the lanes parameter does), so this is fully compatible with existing keys. Apple M-series (OpenSSL 3.6.3), borg defaults (t=3, m=64 MiB, p=4): threads=1: 83 ms, threads=4: 29 ms (2.8x faster). Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
775ec8aac0
commit
8d81cfabb7
@@ -87,7 +87,7 @@ jobs:
|
||||
python-version: '3.13'
|
||||
|
||||
# The wheels pip has to build from source inside the emulated container
|
||||
# (msgpack, argon2-cffi-bindings, borghash, borgstore) are the expensive
|
||||
# (msgpack, borghash, borgstore) are the expensive
|
||||
# part of the container setup, so keep them from run to run.
|
||||
- name: Cache pip-built wheels (s390x)
|
||||
uses: actions/cache@v6
|
||||
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
|
||||
- name: Build python venv
|
||||
run: |
|
||||
# building cffi / argon2-cffi in the venv fails, so we try to use the system packages
|
||||
# building some packages 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
|
||||
|
||||
@@ -645,8 +645,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
|
||||
@@ -750,7 +749,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
|
||||
|
||||
@@ -168,10 +168,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.
|
||||
|
||||
@@ -335,7 +335,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
|
||||
|
||||
@@ -36,11 +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",
|
||||
# 26.x builds via scikit-build-core and thus needs cmake, which has no wheels for
|
||||
# the BSDs / OmniOS - pip builds it from source there for ~30min, then fails.
|
||||
# Temporary: argon2 is moving to OpenSSL, which drops argon2-cffi entirely, see #9602.
|
||||
"argon2-cffi-bindings == 25.1.0",
|
||||
"shtab>=1.9.3", # older versions generate broken zsh (tqdm/shtab#224) or fish (tqdm/shtab#227) completions
|
||||
"backports-zstd; python_version < '3.14'", # for python < 3.14.
|
||||
"jsonargparse>=4.47.0",
|
||||
|
||||
@@ -12,7 +12,6 @@ echo "Building single-file binary of borgbackup..."
|
||||
# Run Nuitka compilation
|
||||
# We use --assume-yes-for-downloads to avoid interactive prompts in automated runs.
|
||||
# We set PYTHONPATH=src to ensure the local version of borg is used.
|
||||
# We include cffi to avoid runtime ModuleNotFoundError in argon2-cffi.
|
||||
mkdir -p $OUTPUT_DIR
|
||||
PYTHONPATH=src python -m nuitka \
|
||||
--mode=onefile \
|
||||
@@ -20,7 +19,6 @@ PYTHONPATH=src python -m nuitka \
|
||||
--include-package=borg \
|
||||
--include-package=borghash \
|
||||
--include-package=borgstore \
|
||||
--include-package=cffi \
|
||||
--output-dir="$OUTPUT_DIR" \
|
||||
--output-filename="$OUTPUT_FILENAME" \
|
||||
"$SRC_DIR"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -159,7 +159,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
|
||||
@@ -171,7 +171,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)
|
||||
|
||||
+2
-11
@@ -13,7 +13,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
|
||||
@@ -577,17 +576,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(
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
# XXH64: only used to read borg 1.x repo integrity data on `borg transfer`, see #9935.
|
||||
def xxh64(data: bytes, seed: int = 0) -> bytes: ...
|
||||
|
||||
@@ -39,6 +39,7 @@ the legacy AES-CTR ones a mac_key and an enc_key):
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
from math import ceil
|
||||
|
||||
from cpython cimport PyMem_Malloc, PyMem_Free
|
||||
@@ -88,6 +89,37 @@ cdef extern from "openssl/evp.h" nogil:
|
||||
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[])
|
||||
|
||||
cdef extern from "openssl/thread.h":
|
||||
int OSSL_set_max_threads(void *ctx, uint64_t max_threads)
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
@@ -1027,3 +1059,58 @@ cdef class XXH64:
|
||||
def xxh64(data, seed=0):
|
||||
"""One-shot XXH64: return the 8-byte canonical (big-endian) digest of *data*."""
|
||||
return XXH64(data, seed).digest()
|
||||
|
||||
|
||||
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")
|
||||
|
||||
# threads only affect speed, not the result; must be <= lanes (parallelism)
|
||||
threads = min(parallelism, os.cpu_count() or 1)
|
||||
if threads > 1 and not OSSL_set_max_threads(NULL, threads):
|
||||
threads = 1 # OpenSSL was built without thread support
|
||||
|
||||
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, <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
|
||||
|
||||
@@ -6,7 +6,7 @@ import unittest
|
||||
|
||||
from ...crypto.low_level import AES256_CTR_HMAC_SHA256, AES256_OCB, CHACHA20_POLY1305, UNENCRYPTED, IntegrityError
|
||||
from ...crypto.low_level import bytes_to_long, bytes_to_int, long_to_bytes
|
||||
from ...crypto.low_level import hmac_sha256
|
||||
from ...crypto.low_level import hmac_sha256, argon2_hash
|
||||
from ...legacy.crypto.low_level import AES
|
||||
from hashlib import sha256
|
||||
from ...crypto.key import CHPOKey, AESOCBKey, KeyBase, ChecksumKey
|
||||
@@ -224,6 +224,13 @@ class CryptoTestCase(BaseTestCase):
|
||||
self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata, aad=b"incorrect_chunkid"))
|
||||
|
||||
|
||||
def test_argon2_hash_parallel():
|
||||
# parallelism > 1 exercises the multi-threaded lane computation on multi-core machines.
|
||||
# reference value cross-checked against argon2-cffi; the result must not depend on thread count.
|
||||
out = argon2_hash(b"pass", b"saltsaltsaltsalt", 3, 65536, 4, 32, "id")
|
||||
assert out.hex() == "bf1a482d5b69a34f3922bcc5e66747486b0527336f483e13477b0c484c8932ea"
|
||||
|
||||
|
||||
def test_decrypt_key_file_argon2_chacha20_poly1305():
|
||||
plain = b"hello"
|
||||
# echo -n "hello, pass phrase" | argon2 saltsaltsaltsalt -id -t 1 -k 8 -p 1 -l 32 -r
|
||||
|
||||
Reference in New Issue
Block a user