Repository: borgbackup/borg

Issue:
blake2b as MAC layer for authenticated encryption.
way faster than hmac-sha256, one of borg's biggest cpu hogs.

---

:moneybag: [there is a bounty for this](https://www.bountysource.com/issues/39600312-blake2b-as-mac-layer-for-authenticated-encryption)


Discussion:

Resolving patch:
diff --git a/src/borg/key.py b/src/borg/key.py
--- a/src/borg/key.py
+++ b/src/borg/key.py
@@ -161,6 +161,20 @@ def decrypt(self, id, data, decompress=True):
         return Chunk(data)
 
 
+def random_blake2b_256_key():
+    # This might look a bit curious, but is the same construction used in the keyed mode of BLAKE2b.
+    # Why limit the key to 64 bytes and pad it with 64 nulls nonetheless? The answer is that BLAKE2b
+    # has a 128 byte block size, but only 64 bytes of internal state (this is also referred to as a
+    # "local wide pipe" design, because the compression function transforms (block, state) => state,
+    # and len(block) >= len(state), hence wide.)
+    # In other words, a key longer than 64 bytes would have simply no advantage, since the function
+    # has no way of propagating more than 64 bytes of entropy internally.
+    # It's padded to a full block so that the key is never buffered internally by blake2b_update, ie.
+    # it remains in a single memory location that can be tracked and could be erased securely, if we
+    # wanted to.
+    return os.urandom(64) + bytes(64)
+
+
 class ID_BLAKE2b_256:
     """
     Key mix-in class for using BLAKE2b-256 for the id key.
@@ -171,6 +185,12 @@ class ID_BLAKE2b_256:
     def id_hash(self, data):
         return blake2b_256(self.id_key, data)
 
+    def init_from_random_data(self, data=None):
+        assert data is None  # PassphraseKey is the only caller using *data*
+        super().init_from_random_data()
+        self.enc_hmac_key = random_blake2b_256_key()
+        self.id_key = random_blake2b_256_key()
+
 
 class ID_HMAC_SHA_256:
     """
@@ -198,12 +218,14 @@ class AESKeyBase(KeyBase):
 
     PAYLOAD_OVERHEAD = 1 + 32 + 8  # TYPE + HMAC + NONCE
 
+    MAC = hmac_sha256
+
     def encrypt(self, chunk):
         chunk = self.compress(chunk)
         self.nonce_manager.ensure_reservation(num_aes_blocks(len(chunk.data)))
         self.enc_cipher.reset()
         data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(chunk.data)))
-        hmac = hmac_sha256(self.enc_hmac_key, data)
+        hmac = self.MAC(self.enc_hmac_key, data)
         return b''.join((self.TYPE_STR, hmac, data))
 
     def decrypt(self, id, data, decompress=True):
@@ -212,7 +234,7 @@ def decrypt(self, id, data, decompress=True):
             raise IntegrityError('Chunk %s: Invalid encryption envelope' % bin_to_hex(id))
         data_view = memoryview(data)
         hmac_given = data_view[1:33]
-        hmac_computed = memoryview(hmac_sha256(self.enc_hmac_key, data_view[33:]))
+        hmac_computed = memoryview(self.MAC(self.enc_hmac_key, data_view[33:]))
         if not compare_digest(hmac_computed, hmac_given):
             raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % bin_to_hex(id))
         self.dec_cipher.reset(iv=PREFIX + data[33:41])
@@ -230,7 +252,9 @@ def extract_nonce(self, payload):
         nonce = bytes_to_long(payload[33:41])
         return nonce
 
-    def init_from_random_data(self, data):
+    def init_from_random_data(self, data=None):
+        if data is None:
+            data = os.urandom(100)
         self.enc_key = data[0:32]
         self.enc_hmac_key = data[32:64]
         self.id_key = data[64:96]
@@ -457,7 +481,7 @@ def create(cls, repository, args):
         passphrase = Passphrase.new(allow_empty=True)
         key = cls(repository)
         key.repository_id = repository.id
-        key.init_from_random_data(os.urandom(100))
+        key.init_from_random_data()
         key.init_ciphers()
         target = key.get_new_target(args)
         key.save(target, passphrase)
@@ -568,11 +592,13 @@ class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey):
     TYPE = 0x04
     NAME = 'key file BLAKE2b'
     FILE_ID = 'BORG_KEY'
+    MAC = blake2b_256
 
 
 class Blake2RepoKey(ID_BLAKE2b_256, RepoKey):
     TYPE = 0x05
     NAME = 'repokey BLAKE2b'
+    MAC = blake2b_256
 
 
 class AuthenticatedKey(ID_BLAKE2b_256, RepoKey):
diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py
--- a/src/borg/testsuite/key.py
+++ b/src/borg/testsuite/key.py
@@ -45,8 +45,8 @@ class MockArgs:
         zKvtzupDyTsfrJMqppdGVyYXRpb25zzgABhqCkc2FsdNoAIGTK3TR09UZqw1bPi17gyHOi
         7YtSp4BVK7XptWeKh6Vip3ZlcnNpb24B""".strip()
 
-    keyfile_blake2_cdata = bytes.fromhex('04dd21cc91140ef009bc9e4dd634d075e39d39025ccce1289c'
-                                         '5536f9cb57f5f8130404040404040408ec852921309243b164')
+    keyfile_blake2_cdata = bytes.fromhex('045d225d745e07af9002d739391e4e7509ff82a04f98debd74'
+                                         '012f09b82cc1d07e0404040404040408ec852921309243b164')
     # Verified against b2sum. Entire string passed to BLAKE2, including the 32 byte key contained in
     # keyfile_blake2_key_file above is
     # 037fb9b75b20d623f1d5a568050fccde4a1b7c5f5047432925e941a17c7a2d0d7061796c6f6164


Test patch: