Skip to content

Commit 9a65d52

Browse files
Merge pull request #8903 from ThomasWaldmann/buzhash64
buzhash64 chunker
2 parents 6487a98 + d23704e commit 9a65d52

17 files changed

Lines changed: 519 additions & 16 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ src/borg/compress.c
77
src/borg/crypto/low_level.c
88
src/borg/item.c
99
src/borg/chunkers/buzhash.c
10+
src/borg/chunkers/buzhash64.c
1011
src/borg/chunkers/reader.c
1112
src/borg/checksums.c
1213
src/borg/platform/darwin.c

docs/internals.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ specified when the backup was performed.
1919
Deduplication is performed globally across all data in the repository
2020
(multiple backups and even multiple hosts), both on data and file
2121
metadata, using :ref:`chunks` created by the chunker using the
22-
Buzhash_ algorithm ("buzhash" chunker) or a simpler fixed blocksize
23-
algorithm ("fixed" chunker).
22+
Buzhash_ algorithm ("buzhash" and "buzhash64" chunker) or a simpler
23+
fixed blocksize algorithm ("fixed" chunker).
2424

2525
To perform the repository-wide deduplication, a hash of each
2626
chunk is checked against the :ref:`chunks cache <cache>`, which is a

docs/internals/data-structures.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ Borg has these chunkers:
399399
supporting a header block of different size.
400400
- "buzhash": variable, content-defined blocksize, uses a rolling hash
401401
computed by the Buzhash_ algorithm.
402+
- "buzhash64": similar to "buzhash", but improved 64bit implementation
402403

403404
For some more general usage hints see also ``--chunker-params``.
404405

@@ -469,6 +470,16 @@ for the repository, and stored encrypted in the keyfile. This is to prevent
469470
chunk size based fingerprinting attacks on your encrypted repo contents (to
470471
guess what files you have based on a specific set of chunk sizes).
471472

473+
"buzhash64" chunker
474+
+++++++++++++++++++
475+
476+
Similar to "buzhash", but using 64bit wide hash values.
477+
478+
The buzhash table is cryptographically derived from secret key material.
479+
480+
These changes should improve resistance against attacks and also solve
481+
some of the issues of the original (32bit / XORed table) implementation.
482+
472483
.. _cache:
473484

474485
The cache

docs/internals/security.rst

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,13 +361,19 @@ The chunks stored in the repo are the (compressed, encrypted and authenticated)
361361
output of the chunker. The sizes of these stored chunks are influenced by the
362362
compression, encryption and authentication.
363363

364-
buzhash chunker
365-
~~~~~~~~~~~~~~~
364+
buzhash and buzhash64 chunker
365+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
366366

367-
The buzhash chunker chunks according to the input data, the chunker's
368-
parameters and the secret chunker seed (which all influence the chunk boundary
367+
The buzhash chunkers chunk according to the input data, the chunker's
368+
parameters and secret key material (which all influence the chunk boundary
369369
positions).
370370

371+
Secret key material:
372+
373+
- "buzhash": chunker seed (32bits), used for XORing the hardcoded buzhash table
374+
- "buzhash64": bh64_key (256bits) is derived from ID key, used to cryptographically
375+
generate the table.
376+
371377
Small files below some specific threshold (default: 512 KiB) result in only one
372378
chunk (identical content / size as the original file), bigger files result in
373379
multiple chunks.

scripts/make.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ def is_positional_group(group):
543543
src/borg/compress.pyx
544544
src/borg/crypto/low_level.pyx
545545
src/borg/chunkers/buzhash.pyx
546+
src/borg/chunkers/buzhash64.pyx
546547
src/borg/chunkers/reader.pyx
547548
src/borg/hashindex.pyx
548549
src/borg/item.pyx

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
compress_source = "src/borg/compress.pyx"
5252
crypto_ll_source = "src/borg/crypto/low_level.pyx"
5353
buzhash_source = "src/borg/chunkers/buzhash.pyx"
54+
buzhash64_source = "src/borg/chunkers/buzhash64.pyx"
5455
reader_source = "src/borg/chunkers/reader.pyx"
5556
hashindex_source = "src/borg/hashindex.pyx"
5657
item_source = "src/borg/item.pyx"
@@ -66,6 +67,7 @@
6667
compress_source,
6768
crypto_ll_source,
6869
buzhash_source,
70+
buzhash64_source,
6971
reader_source,
7072
hashindex_source,
7173
item_source,
@@ -185,6 +187,7 @@ def lib_ext_kwargs(pc, prefix_env_var, lib_name, lib_pkg_name, pc_version, lib_s
185187
Extension("borg.hashindex", [hashindex_source], extra_compile_args=cflags),
186188
Extension("borg.item", [item_source], extra_compile_args=cflags),
187189
Extension("borg.chunkers.buzhash", [buzhash_source], extra_compile_args=cflags, undef_macros=["NDEBUG"]),
190+
Extension("borg.chunkers.buzhash64", [buzhash64_source], extra_compile_args=cflags, undef_macros=["NDEBUG"]),
188191
Extension("borg.chunkers.reader", [reader_source], extra_compile_args=cflags, undef_macros=["NDEBUG"]),
189192
Extension("borg.checksums", **checksums_ext_kwargs),
190193
]

src/borg/archive.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ def __init__(self, key, chunker_params=ITEMS_CHUNKER_PARAMS):
351351
self.packer = msgpack.Packer()
352352
self.chunks = []
353353
self.key = key
354-
self.chunker = get_chunker(*chunker_params, seed=self.key.chunk_seed, sparse=False)
354+
self.chunker = get_chunker(*chunker_params, key=self.key, sparse=False)
355355
self.saved_chunks_len = None
356356

357357
def add(self, item):
@@ -1227,7 +1227,7 @@ def __init__(
12271227
self.hlm = HardLinkManager(id_type=tuple, info_type=(list, type(None))) # (dev, ino) -> chunks or None
12281228
self.stats = Statistics(output_json=log_json, iec=iec) # threading: done by cache (including progress)
12291229
self.cwd = os.getcwd()
1230-
self.chunker = get_chunker(*chunker_params, seed=key.chunk_seed, sparse=sparse)
1230+
self.chunker = get_chunker(*chunker_params, key=key, sparse=sparse)
12311231

12321232
@contextmanager
12331233
def create_helper(self, path, st, status=None, hardlinkable=True, strip_prefix=None):
@@ -1502,7 +1502,7 @@ def __init__(
15021502
self.print_file_status = file_status_printer or (lambda *args: None)
15031503

15041504
self.stats = Statistics(output_json=log_json, iec=iec) # threading: done by cache (including progress)
1505-
self.chunker = get_chunker(*chunker_params, seed=key.chunk_seed, sparse=False)
1505+
self.chunker = get_chunker(*chunker_params, key=key, sparse=False)
15061506
self.hlm = HardLinkManager(id_type=str, info_type=list) # path -> chunks
15071507

15081508
@contextmanager
@@ -2325,7 +2325,7 @@ def create_target(self, archive, target_name):
23252325
target.process_file_chunks = ChunksProcessor(
23262326
cache=self.cache, key=self.key, add_item=target.add_item, rechunkify=target.recreate_rechunkify
23272327
).process_file_chunks
2328-
target.chunker = get_chunker(*target.chunker_params, seed=self.key.chunk_seed, sparse=False)
2328+
target.chunker = get_chunker(*target.chunker_params, key=self.key, sparse=False)
23292329
return target
23302330

23312331
def create_target_archive(self, name):

src/borg/archiver/benchmark_cmd.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ def chunkit(chunker_name, *args, **kwargs):
146146
pass
147147

148148
for spec, func in [
149-
("buzhash,19,23,21,4095", lambda: chunkit("buzhash", 19, 23, 21, 4095, seed=0, sparse=False)),
149+
("buzhash,19,23,21,4095", lambda: chunkit("buzhash", 19, 23, 21, 4095, sparse=False)),
150+
("buzhash64,19,23,21,4095", lambda: chunkit("buzhash64", 19, 23, 21, 4095, sparse=False)),
150151
("fixed,1048576", lambda: chunkit("fixed", 1048576, sparse=False)),
151152
]:
152153
print(f"{spec:<24} {size:<10} {timeit(func, number=100):.3f}s")

src/borg/archiver/transfer_cmd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def transfer_chunks(
4141
file = ChunkIteratorFileWrapper(chunk_iterator)
4242

4343
# Create a chunker with the specified parameters
44-
chunker = get_chunker(*chunker_params, seed=archive.key.chunk_seed, sparse=False)
44+
chunker = get_chunker(*chunker_params, key=archive.key, sparse=False)
4545
for chunk in chunker.chunkify(file):
4646
if not dry_run:
4747
chunk_id, data = cached_hash(chunk, archive.key.id_hash)

src/borg/chunkers/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .buzhash import Chunker
2+
from .buzhash64 import ChunkerBuzHash64
23
from .failing import ChunkerFailing
34
from .fixed import ChunkerFixed
45
from .reader import * # noqa
@@ -7,12 +8,17 @@
78

89

910
def get_chunker(algo, *params, **kw):
11+
key = kw.get("key", None)
12+
sparse = kw.get("sparse", False)
13+
# key.chunk_seed only has 32bits
14+
seed = key.chunk_seed if key is not None else 0
15+
# for buzhash64, we want a much longer key, so we derive it from the id key
16+
bh64_key = key.derive_key(salt=b"", domain=b"buzhash64", size=32, from_id_key=True) if key is not None else b""
1017
if algo == "buzhash":
11-
seed = kw["seed"]
12-
sparse = kw["sparse"]
1318
return Chunker(seed, *params, sparse=sparse)
19+
if algo == "buzhash64":
20+
return ChunkerBuzHash64(bh64_key, *params, sparse=sparse)
1421
if algo == "fixed":
15-
sparse = kw["sparse"]
1622
return ChunkerFixed(*params, sparse=sparse)
1723
if algo == "fail":
1824
return ChunkerFailing(*params)

0 commit comments

Comments
 (0)