Crypto-View

borgbackup/borg

Cryptographic posture

89 cryptographic locations: 1 already broken, 58 reduced-margin, 1 undetermined, 29 quantum-safe

294 files analysed.

Already broken 1 Reduced margin 58 Could not be determined 1 Quantum-safe 29
To address2
Key establishment0
Inventory only0
Total findings89
What was analysed
Branch master
Commit 177d09c9152dcf983f3f3c918bc5ec630cfc7426 Merge pull request #10220 from ThomasWaldmann/import-tar-strip-components-6461
Committed 2026-09-20 17:38 UTC
Scanned 2026-09-21 10:58 UTC 9 hours ago
Coverage 294 files, 246 python

List of cryptographic assets

MD5 Already broken 1 place See details

Broken hash function

`hashlib.md5()` or `hashlib.sha1()`. Both have practical collisions. Where the call is annotated `usedforsecurity=False` the finding is recorded as inventory instead.

This is already unsafe today, with no quantum computer involved. Practical chosen-prefix collisions exist; MD5 has no remaining security as a digest.

What to do. SHA-256, or SHA-384 for long-lived signatures.

  1. src/borg/testsuite/helpers/datastruct_test.py:11 test path assert hashlib.md5(msgpack.packb(d)).hexdigest() == "fc78df42cd60691b3ac3dd2a2b39903f"
py.hashlib.weak · CWE-328
SHA-256 Reduced margin Renamed import 31 places See details

Hash function in use

`hashlib.sha256()` and friends. The digest is read from the call, so SHA-384 and above are inventory while the broken ones are reported separately.

A quantum computer weakens this but does not break it. Increasing the key or digest size restores the margin. Pre-image resistance falls to about 128 bits of quantum work. Adequate for most uses; SHA-384 restores the full margin where a signature must last decades.

What to do. SHA-256 as the floor, SHA-384 where the digest protects something long-lived.

  1. scripts/chunker_bench.py:144 hashes.append(hashlib.sha256(b"\0" * n).digest())
  2. scripts/chunker_bench.py:148 hashes.append(hashlib.sha256(b).digest())
  3. scripts/endian_interop_test.py:138 test path digest = hashlib.sha256()
  4. src/borg/crypto/key.py:443 return sha256(from_key + salt + domain).digest()[:size]
  5. src/borg/crypto/keymanager.py:41 h = sha256()
  6. src/borg/helpers/fs.py:391 return hashlib.sha256(path.encode("utf-8", errors="surrogateescape")).digest()
  7. src/borg/helpers/fs.py:397 return hashlib.sha256(f"{ino}/{dev}".encode()).digest()
  8. src/borg/helpers/parseformat.py:1276 self.conditions_hash = sha256(conditions.encode()).hexdigest()
  9. src/borg/helpers/parseformat.py:1345 chunks_hash = sha256(b"".join(c.id for c in chunks)).hexdigest()
  10. src/borg/legacy/crypto/key.py:121 return sha256(data).digest()
  11. src/borg/testsuite/archiver/create_cmd_test.py:1993 test path "sha256": hashlib.sha256(contents).digest(),
  12. src/borg/testsuite/archiver/tar_cmds_test.py:241 test path assert digests["dir/file1"] == {"sha256": hashlib.sha256(contents["dir/file1"]).digest()}
  13. src/borg/testsuite/archiver/transfer_cmd_test.py:587 test path source_file_hashes[item.path] = hashlib.sha256(content).hexdigest()
  14. src/borg/testsuite/archiver/transfer_cmd_test.py:612 test path dest_hash = hashlib.sha256(content).hexdigest()
  15. src/borg/testsuite/cache_test.py:413 test path return hashlib.sha256(i.to_bytes(4, "big")).digest()
  16. src/borg/testsuite/chunkers/buzhash64_test.py:22 test path return sha256(data).digest()
  17. src/borg/testsuite/chunkers/buzhash_test.py:15 test path return sha256(data).digest()
  18. src/borg/testsuite/chunkers/fastcdc_test.py:24 test path return sha256(data).digest()
  19. src/borg/testsuite/chunkers/goldilocks_aes_test.py:26 test path return sha256(data).digest()
  20. src/borg/testsuite/chunkers/phte_chunkers_test.py:41 test path return sha256(data).digest()
  21. src/borg/testsuite/chunkers/rabin_aes_test.py:18 test path return sha256(data).digest()
  22. src/borg/testsuite/chunkers/toeplitz_aes_test.py:20 test path return sha256(data).digest()
  23. src/borg/testsuite/crypto/crypto_test.py:336 test path expected = sha256(crypt_key + salt + domain).digest()[:size]
  24. src/borg/testsuite/crypto/crypto_test.py:357 test path expected = sha256(id_key + salt + domain).digest()[:size]
  25. src/borg/testsuite/crypto/file_integrity_test.py:164 test path assert wrapper.hexdigest() == hashlib.sha256(data).hexdigest()
  26. src/borg/testsuite/crypto/file_integrity_test.py:171 test path assert wrapper.hexdigest() == hashlib.sha256(data).hexdigest()
  27. src/borg/testsuite/crypto/file_integrity_test.py:179 test path expected_hash = hashlib.sha256(data + b"11").hexdigest()
  28. src/borg/testsuite/digests_test.py:112 test path "sha256": hashlib.sha256(b"foobar").digest(),
  29. src/borg/testsuite/hashindex_test.py:17 test path return hashlib.sha256(H(x)).digest()
  30. src/borg/vfs.py:388 file_id = hashlib.sha256(path.encode("utf-8", "surrogateescape")).digest()[:16]
  31. src/borg/vfs.py:390 contents_id = hashlib.sha256(b"".join(chunk_id for chunk_id, _ in item.chunks)).digest()[:16]
py.hashlib
unknown Could not be determined 1 place See details

JWT signed with a classical algorithm

A JWT `algorithm=` value. RS*, PS*, ES* and EdDSA all sign with a quantum-vulnerable key; HS* is an HMAC and is not affected.

The algorithm could not be established from the source. Usually it is selected while the program runs, but it may equally be that the call gives no clue, or that it is decided somewhere this scan does not reach. Somebody has to check.

What to do. There is no standardised post-quantum JOSE algorithm yet. Track the IETF work and keep the token lifetime short, which limits the value of a forged token.

py.pyjwt.algorithm · CWE-327
CSPRNG Quantum-safe 25 places See details

Random number generation

`secrets.token_bytes()`, `os.urandom()` or `ssl.RAND_bytes()`. Recorded so the inventory names its randomness source.

This is where the software gets its random numbers. Quantum computers do not weaken it.

What to do. No action. `random.random()` is a separate, present-day defect if it reaches key material.

  1. scripts/chunker_bench.py:25 --synthetic random:N N bytes of os.urandom (incompressible, worst case)
  2. src/borg/archiver/benchmark_cmd.py:101 data = z_buff if not random else os.urandom(size)
  3. src/borg/archiver/repo_space_cmd.py:26 data = os.urandom(storage_space_reserve_object_size) # counter-act fs compression/dedup
  4. src/borg/crypto/key.py:523 data = os.urandom(100)
  5. src/borg/helpers/fs.py:456 fd.write(os.urandom(st.st_size))
  6. src/borg/legacy/crypto/key.py:49 salt = os.urandom(32)
  7. src/borg/legacy/repository.py:325 config.set("repository", "id", bin_to_hex(os.urandom(32)))
  8. src/borg/repository.py:1029 self.id = os.urandom(32)
  9. src/borg/testsuite/archiver/compact_cmd_test.py:128 test path content = os.urandom(1024 * 1024)
  10. src/borg/testsuite/archiver/create_cmd_test.py:982 test path create_regular_file(archiver.input_path, "file1", contents=os.urandom(1024 * 80))
  11. src/borg/testsuite/archiver/disk_full_test.py:41 test path data = os.urandom(size)
  12. src/borg/testsuite/archiver/remote_repo_test.py:149 test path contents_kept = os.urandom(100 * 1024)
  13. src/borg/testsuite/archiver/repo_compress_cmd_test.py:44 test path create_regular_file(archiver.input_path, "file2", contents=os.urandom(1024 * 10))
  14. src/borg/testsuite/archiver/tar_cmds_test.py:195 test path data = os.urandom(1024 * 80)
  15. src/borg/testsuite/archiver/webdav_cmd_test.py:56 test path big = os.urandom(5 * 1024 * 1024) # big enough for multiple chunks with default chunker params
  16. src/borg/testsuite/benchmark_test.py:51 test path return os.urandom(size)
  17. src/borg/testsuite/chunkers/buzhash64_test.py:57 test path data = os.urandom(1048576)
  18. src/borg/testsuite/chunkers/buzhash_test.py:49 test path data = os.urandom(1048576)
  19. src/borg/testsuite/chunkers/fastcdc_test.py:61 test path data = os.urandom(1048576)
  20. src/borg/testsuite/chunkers/fixed_test.py:91 test path data = os.urandom(size)
  21. src/borg/testsuite/chunkers/goldilocks_aes_test.py:103 test path data = os.urandom(256 * 1024)
  22. src/borg/testsuite/chunkers/interaction_test.py:29 test path random_data = os.urandom(data_size // 3) + b"\0" * (data_size // 3) + os.urandom(data_size // 3)
  23. src/borg/testsuite/chunkers/phte_chunkers_test.py:58 test path data = os.urandom(4 * 1024 * 1024)
  24. src/borg/testsuite/compress_test.py:49 test path incompressible_data = os.urandom(5 * 2**20) * 10 # 50MiB badly compressible data
  25. src/borg/testsuite/digests_test.py:35 test path pieces = [os.urandom(ContentDigester.THREAD_MIN_CHUNK_SIZE) for _ in range(5)]
py.rng
HMAC Quantum-safe 1 place See details

Keyed hash in use

`hmac.new()` or `hmac.digest()`. Not broken by Shor and only marginally affected by Grover. Recorded so the inventory is complete.

This is a fingerprint proving a message was not altered. Quantum computers do not break it.

What to do. No action, unless the digest is MD5 or SHA-1.

  1. src/borg/crypto/key.py:1251 h = hmac.new(self.tag_key, prefix, sha256)
py.hmac

Cryptographic assets

Algorithm Assessment What it means Occurrences
SHA-256 Reduced margin Pre-image resistance falls to about 128 bits of quantum work. Adequate for most uses; SHA-384 restores the full margin where a signature must last decades. 58
CSPRNG Quantum-safe A cryptographically secure random number generator provided by the platform. Not weakened by a quantum computer. 25
HMAC Quantum-safe A keyed MAC is not affected by Shor and only marginally by Grover. 4
MD5 Already broken Practical chosen-prefix collisions exist; MD5 has no remaining security as a digest. 1
unknown Could not be determined The algorithm could not be established from the source - chosen at runtime, or decided somewhere this scan does not reach. 1

Imported cryptographic libraries

Library Files
hashlib the standard digests, including the broken ones 27
hmac keyed message authentication 3