mirror of https://github.com/borgbackup/borg.git
Merge pull request #7955 from ThomasWaldmann/improve-session-key-gen-master
crypto: use a one-step kdf for session keys, fixes #7953
This commit is contained in:
commit
812eb352f6
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 129 KiB |
|
@ -124,7 +124,8 @@ The chunk ID is derived via a MAC over the plaintext (mac key taken from borg ke
|
|||
For each borg invocation, a new session id is generated by `os.urandom`_.
|
||||
|
||||
From that session id, the initial key material (ikm, taken from the borg key)
|
||||
and an application and cipher specific salt, borg derives a session key via HKDF.
|
||||
and an application and cipher specific salt, borg derives a session key using a
|
||||
"one-step KDF" based on just sha256.
|
||||
|
||||
For each session key, IVs (nonces) are generated by a counter which increments for
|
||||
each encrypted message.
|
||||
|
@ -132,9 +133,8 @@ each encrypted message.
|
|||
Session::
|
||||
|
||||
sessionid = os.urandom(24)
|
||||
ikm = crypt_key
|
||||
salt = "borg-session-key-CIPHERNAME"
|
||||
sessionkey = HKDF(ikm, sessionid, salt)
|
||||
domain = "borg-session-key-CIPHERNAME"
|
||||
sessionkey = sha256(crypt_key + sessionid + domain)
|
||||
message_iv = 0
|
||||
|
||||
Encryption::
|
||||
|
@ -155,7 +155,9 @@ Decryption::
|
|||
|
||||
ASSERT(type-byte is correct)
|
||||
|
||||
past_key = HKDF(ikm, past_sessionid, salt)
|
||||
domain = "borg-session-key-CIPHERNAME"
|
||||
past_key = sha256(crypt_key + past_sessionid + domain)
|
||||
|
||||
decrypted = AEAD_decrypt(past_key, past_message_iv, authenticated)
|
||||
|
||||
decompressed = decompress(decrypted)
|
||||
|
@ -229,12 +231,7 @@ on widely used libraries providing them:
|
|||
- HMAC and a constant-time comparison from Python's hmac_ standard library module are used.
|
||||
- argon2 is used via argon2-cffi.
|
||||
|
||||
Implemented cryptographic constructions are:
|
||||
|
||||
- HKDF_-SHA-512 (using ``hmac.digest`` from Python's hmac_ standard library module)
|
||||
|
||||
.. _Horton principle: https://en.wikipedia.org/wiki/Horton_Principle
|
||||
.. _HKDF: https://tools.ietf.org/html/rfc5869
|
||||
.. _length extension: https://en.wikipedia.org/wiki/Length_extension_attack
|
||||
.. _hashlib: https://docs.python.org/3/library/hashlib.html
|
||||
.. _hmac: https://docs.python.org/3/library/hmac.html
|
||||
|
|
|
@ -27,7 +27,7 @@ from ..platform import SaveFile
|
|||
from ..repoobj import RepoObj
|
||||
|
||||
|
||||
from .low_level import AES, bytes_to_int, num_cipher_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512
|
||||
from .low_level import AES, bytes_to_int, num_cipher_blocks, hmac_sha256, blake2b_256
|
||||
from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b, AES256_OCB, CHACHA20_POLY1305
|
||||
from . import low_level
|
||||
|
||||
|
@ -833,7 +833,7 @@ class AEADKeyBase(KeyBase):
|
|||
# to decrypt existing data, we need to get a cipher configured for the sessionid and iv from header
|
||||
self.assert_type(data[0], id)
|
||||
iv_48bit = data[2:8]
|
||||
sessionid = data[8:32]
|
||||
sessionid = bytes(data[8:32])
|
||||
iv = int.from_bytes(iv_48bit, "big")
|
||||
cipher = self._get_cipher(sessionid, iv)
|
||||
try:
|
||||
|
@ -857,15 +857,22 @@ class AEADKeyBase(KeyBase):
|
|||
chunk_seed = chunk_seed - 0xFFFFFFFF - 1
|
||||
self.init_from_given_data(crypt_key=data[0:64], id_key=data[64:96], chunk_seed=chunk_seed)
|
||||
|
||||
def _get_session_key(self, sessionid):
|
||||
def _get_session_key(self, sessionid, domain=None):
|
||||
"""
|
||||
Derive a session key from the secret long-term static crypt_key (which is a fully random PRK)
|
||||
and the session id (which is fully random also).
|
||||
Optionally, a domain can be given for domain separation (defaults to a different binary string
|
||||
per cipher suite).
|
||||
"""
|
||||
# Performance note:
|
||||
# While this is only invoked once per session to generate a new key for encrypting new data, it is invoked
|
||||
# frequently (per encrypted repo object) to compute the corresponding key for decrypting existing data.
|
||||
assert len(sessionid) == 24 # 192bit
|
||||
key = hkdf_hmac_sha512(
|
||||
ikm=self.crypt_key,
|
||||
salt=sessionid,
|
||||
info=b"borg-session-key-" + self.CIPHERSUITE.__name__.encode(),
|
||||
output_length=32,
|
||||
)
|
||||
return key
|
||||
if domain is None:
|
||||
domain = b"borg-session-key-" + self.CIPHERSUITE.__name__.encode()
|
||||
# Because crypt_key is already a PRK, we do not need KDF security here, PRF security is good enough.
|
||||
# See https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Cr2.pdf section 4 "one-step KDF".
|
||||
return sha256(self.crypt_key + sessionid + domain).digest()
|
||||
|
||||
def _get_cipher(self, sessionid, iv):
|
||||
assert isinstance(iv, int)
|
||||
|
|
|
@ -714,30 +714,3 @@ def blake2b_256(key, data):
|
|||
|
||||
def blake2b_128(data):
|
||||
return hashlib.blake2b(data, digest_size=16).digest()
|
||||
|
||||
|
||||
def hkdf_hmac_sha512(ikm, salt, info, output_length):
|
||||
"""
|
||||
Compute HKDF-HMAC-SHA512 with input key material *ikm*, *salt* and *info* to produce *output_length* bytes.
|
||||
|
||||
This is the "HMAC-based Extract-and-Expand Key Derivation Function (HKDF)" (RFC 5869)
|
||||
instantiated with HMAC-SHA512.
|
||||
|
||||
*output_length* must not be greater than 64 * 255 bytes.
|
||||
"""
|
||||
digest_length = 64
|
||||
assert output_length <= (255 * digest_length), 'output_length must be <= 255 * 64 bytes'
|
||||
# Step 1. HKDF-Extract (ikm, salt) -> prk
|
||||
if salt is None:
|
||||
salt = bytes(64)
|
||||
prk = hmac.digest(salt, ikm, 'sha512')
|
||||
|
||||
# Step 2. HKDF-Expand (prk, info, output_length) -> output key
|
||||
n = ceil(output_length / digest_length)
|
||||
t_n = b''
|
||||
output = b''
|
||||
for i in range(n):
|
||||
msg = t_n + info + (i + 1).to_bytes(1, 'little')
|
||||
t_n = hmac.digest(prk, msg, 'sha512')
|
||||
output += t_n
|
||||
return output[:output_length]
|
||||
|
|
|
@ -33,7 +33,7 @@ SELFTEST_CASES = [
|
|||
ChunkerTestCase,
|
||||
]
|
||||
|
||||
SELFTEST_COUNT = 38
|
||||
SELFTEST_COUNT = 33
|
||||
|
||||
|
||||
class SelfTestResult(TestResult):
|
||||
|
|
|
@ -7,7 +7,6 @@ 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 hkdf_hmac_sha512
|
||||
from ..crypto.low_level import AES, hmac_sha256
|
||||
from ..crypto.key import CHPOKeyfileKey, AESOCBRepoKey, FlexiKey
|
||||
from ..helpers import msgpack
|
||||
|
@ -195,74 +194,6 @@ class CryptoTestCase(BaseTestCase):
|
|||
cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=0)
|
||||
self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata, aad=b"incorrect_chunkid"))
|
||||
|
||||
# These test vectors come from https://www.kullo.net/blog/hkdf-sha-512-test-vectors/
|
||||
# who claims to have verified these against independent Python and C++ implementations.
|
||||
|
||||
def test_hkdf_hmac_sha512(self):
|
||||
ikm = b"\x0b" * 22
|
||||
salt = bytes.fromhex("000102030405060708090a0b0c")
|
||||
info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9")
|
||||
length = 42
|
||||
|
||||
okm = hkdf_hmac_sha512(ikm, salt, info, length)
|
||||
assert okm == bytes.fromhex(
|
||||
"832390086cda71fb47625bb5ceb168e4c8e26a1a16ed34d9fc7fe92c1481579338da362cb8d9f925d7cb"
|
||||
)
|
||||
|
||||
def test_hkdf_hmac_sha512_2(self):
|
||||
ikm = bytes.fromhex(
|
||||
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627"
|
||||
"28292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f"
|
||||
)
|
||||
salt = bytes.fromhex(
|
||||
"606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868"
|
||||
"788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
|
||||
)
|
||||
info = bytes.fromhex(
|
||||
"b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7"
|
||||
"d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
|
||||
)
|
||||
length = 82
|
||||
|
||||
okm = hkdf_hmac_sha512(ikm, salt, info, length)
|
||||
assert okm == bytes.fromhex(
|
||||
"ce6c97192805b346e6161e821ed165673b84f400a2b514b2fe23d84cd189ddf1b695b48cbd1c838844"
|
||||
"1137b3ce28f16aa64ba33ba466b24df6cfcb021ecff235f6a2056ce3af1de44d572097a8505d9e7a93"
|
||||
)
|
||||
|
||||
def test_hkdf_hmac_sha512_3(self):
|
||||
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
|
||||
salt = None
|
||||
info = b""
|
||||
length = 42
|
||||
|
||||
okm = hkdf_hmac_sha512(ikm, salt, info, length)
|
||||
assert okm == bytes.fromhex(
|
||||
"f5fa02b18298a72a8c23898a8703472c6eb179dc204c03425c970e3b164bf90fff22d04836d0e2343bac"
|
||||
)
|
||||
|
||||
def test_hkdf_hmac_sha512_4(self):
|
||||
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b")
|
||||
salt = bytes.fromhex("000102030405060708090a0b0c")
|
||||
info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9")
|
||||
length = 42
|
||||
|
||||
okm = hkdf_hmac_sha512(ikm, salt, info, length)
|
||||
assert okm == bytes.fromhex(
|
||||
"7413e8997e020610fbf6823f2ce14bff01875db1ca55f68cfcf3954dc8aff53559bd5e3028b080f7c068"
|
||||
)
|
||||
|
||||
def test_hkdf_hmac_sha512_5(self):
|
||||
ikm = bytes.fromhex("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c")
|
||||
salt = None
|
||||
info = b""
|
||||
length = 42
|
||||
|
||||
okm = hkdf_hmac_sha512(ikm, salt, info, length)
|
||||
assert okm == bytes.fromhex(
|
||||
"1407d46013d98bc6decefcfee55f0f90b0c7f63d68eb1a80eaf07e953cfc0a3a5240a155d6e4daa965bb"
|
||||
)
|
||||
|
||||
|
||||
def test_decrypt_key_file_argon2_chacha20_poly1305():
|
||||
plain = b"hello"
|
||||
|
|
Loading…
Reference in New Issue