Browse Source

Add BLAKE2b key types

Marian Beermann 8 years ago
parent
commit
00ac7b14be
5 changed files with 138 additions and 15 deletions
  1. 19 4
      src/borg/archiver.py
  2. 0 0
      src/borg/blake2/openssl-b2.c
  3. 5 0
      src/borg/crypto.pyx
  4. 68 9
      src/borg/key.py
  5. 46 2
      src/borg/testsuite/key.py

+ 19 - 4
src/borg/archiver.py

@@ -959,7 +959,7 @@ class Archiver:
         else:
             encrypted = 'Yes (%s)' % key.NAME
         print('Encrypted: %s' % encrypted)
-        if key.NAME == 'key file':
+        if key.NAME.startswith('key file'):
             print('Key file: %s' % key.find_key())
         print('Cache: %s' % cache.path)
         print(DASHES)
@@ -1556,6 +1556,7 @@ class Archiver:
                                                     'Access to all sub-directories is granted implicitly; PATH doesn\'t need to directly point to a repository.')
         subparser.add_argument('--append-only', dest='append_only', action='store_true',
                                help='only allow appending to repository segment files')
+
         init_epilog = textwrap.dedent("""
         This command initializes an empty repository. A repository is a filesystem
         directory containing the deduplicated data from zero or more archives.
@@ -1599,8 +1600,21 @@ class Archiver:
         You can change your passphrase for existing repos at any time, it won't affect
         the encryption/decryption key or other secrets.
 
-        When encrypting, AES-CTR-256 is used for encryption, and HMAC-SHA256 for
-        authentication. Hardware acceleration will be used automatically.
+        Encryption modes
+        ++++++++++++++++
+
+        repokey and keyfile use AES-CTR-256 for encryption and HMAC-SHA256 for
+        authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash
+        is HMAC-SHA256 as well (with a separate key).
+
+        repokey-blake2 and keyfile-blake2 use the same authenticated encryption, but
+        use a keyed BLAKE2b-256 hash for the chunk ID hash.
+
+        "authenticated" mode uses no encryption, but authenticates repository contents
+        through the same keyed BLAKE2b-256 hash as the other blake2 modes.
+        The key is stored like repokey.
+
+        Hardware acceleration will be used automatically.
         """)
         subparser = subparsers.add_parser('init', parents=[common_parser], add_help=False,
                                           description=self.do_init.__doc__, epilog=init_epilog,
@@ -1611,7 +1625,8 @@ class Archiver:
                                type=location_validator(archive=False),
                                help='repository to create')
         subparser.add_argument('-e', '--encryption', dest='encryption',
-                               choices=('none', 'keyfile', 'repokey'), default='repokey',
+                               choices=('none', 'keyfile', 'repokey', 'keyfile-blake2', 'repokey-blake2', 'authenticated'),
+                               default='repokey',
                                help='select encryption key mode (default: "%(default)s")')
         subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true',
                                help='create an append-only mode repository')

+ 0 - 0
src/borg/blake2/openssl-b2.c


+ 5 - 0
src/borg/crypto.pyx

@@ -231,6 +231,11 @@ def blake2b_256(key, data):
     md = bytes(32)
     cdef unsigned char *md_ptr = md
 
+    # This is secure, because BLAKE2 is not vulnerable to length-extension attacks (unlike SHA-1/2, MD-5 and others).
+    # See the BLAKE2 paper section 2.9 "Keyed hashing (MAC and PRF)" for details.
+    # A nice benefit is that this simpler prefix-MAC mode has less overhead than the more complex HMAC mode.
+    # We don't use the BLAKE2 parameter block (via blake2s_init_key) for this to
+    # avoid incompatibility with the limited API of OpenSSL.
     blake2b_update_from_buffer(&state, key)
     blake2b_update_from_buffer(&state, data)
 

+ 68 - 9
src/borg/key.py

@@ -14,7 +14,7 @@ logger = create_logger()
 
 from .constants import *  # NOQA
 from .compress import Compressor, get_compressor
-from .crypto import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks, hmac_sha256
+from .crypto import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256
 from .helpers import Chunk
 from .helpers import Error, IntegrityError
 from .helpers import yes
@@ -62,6 +62,12 @@ def key_creator(repository, args):
         return KeyfileKey.create(repository, args)
     elif args.encryption == 'repokey':
         return RepoKey.create(repository, args)
+    elif args.encryption == 'keyfile-blake2':
+        return Blake2KeyfileKey.create(repository, args)
+    elif args.encryption == 'repokey-blake2':
+        return Blake2RepoKey.create(repository, args)
+    elif args.encryption == 'authenticated':
+        return AuthenticatedKey.create(repository, args)
     else:
         return PlaintextKey.create(repository, args)
 
@@ -78,6 +84,12 @@ def key_factory(repository, manifest_data):
         return RepoKey.detect(repository, manifest_data)
     elif key_type == PlaintextKey.TYPE:
         return PlaintextKey.detect(repository, manifest_data)
+    elif key_type == Blake2KeyfileKey.TYPE:
+        return Blake2KeyfileKey.detect(repository, manifest_data)
+    elif key_type == Blake2RepoKey.TYPE:
+        return Blake2RepoKey.detect(repository, manifest_data)
+    elif key_type == AuthenticatedKey.TYPE:
+        return AuthenticatedKey.detect(repository, manifest_data)
     else:
         raise UnsupportedPayloadError(key_type)
 
@@ -149,6 +161,28 @@ class PlaintextKey(KeyBase):
         return Chunk(data)
 
 
+class ID_BLAKE2b_256:
+    """
+    Key mix-in class for using BLAKE2b-256 for the id key.
+
+    The id_key length must be 32 bytes.
+    """
+
+    def id_hash(self, data):
+        return blake2b_256(self.id_key, data)
+
+
+class ID_HMAC_SHA_256:
+    """
+    Key mix-in class for using HMAC-SHA-256 for the id key.
+
+    The id_key length must be 32 bytes.
+    """
+
+    def id_hash(self, data):
+        return hmac_sha256(self.id_key, data)
+
+
 class AESKeyBase(KeyBase):
     """Common base class shared by KeyfileKey and PassphraseKey
 
@@ -164,11 +198,6 @@ class AESKeyBase(KeyBase):
 
     PAYLOAD_OVERHEAD = 1 + 32 + 8  # TYPE + HMAC + NONCE
 
-    def id_hash(self, data):
-        """Return HMAC hash using the "id" HMAC key
-        """
-        return hmac_sha256(self.id_key, data)
-
     def encrypt(self, chunk):
         chunk = self.compress(chunk)
         self.nonce_manager.ensure_reservation(num_aes_blocks(len(chunk.data)))
@@ -272,7 +301,7 @@ class Passphrase(str):
         return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
 
 
-class PassphraseKey(AESKeyBase):
+class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase):
     # This mode was killed in borg 1.0, see: https://github.com/borgbackup/borg/issues/97
     # Reasons:
     # - you can never ever change your passphrase for existing repos.
@@ -432,7 +461,7 @@ class KeyfileKeyBase(AESKeyBase):
         raise NotImplementedError
 
 
-class KeyfileKey(KeyfileKeyBase):
+class KeyfileKey(ID_HMAC_SHA_256, KeyfileKeyBase):
     TYPE = 0x00
     NAME = 'key file'
     FILE_ID = 'BORG_KEY'
@@ -492,7 +521,7 @@ class KeyfileKey(KeyfileKeyBase):
         self.target = target
 
 
-class RepoKey(KeyfileKeyBase):
+class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase):
     TYPE = 0x03
     NAME = 'repokey'
 
@@ -522,3 +551,33 @@ class RepoKey(KeyfileKeyBase):
         key_data = key_data.encode('utf-8')  # remote repo: msgpack issue #99, giving bytes
         target.save_key(key_data)
         self.target = target
+
+
+class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey):
+    TYPE = 0x04
+    NAME = 'key file BLAKE2b'
+    FILE_ID = 'BORG_KEY'
+
+
+class Blake2RepoKey(ID_BLAKE2b_256, RepoKey):
+    TYPE = 0x05
+    NAME = 'repokey BLAKE2b'
+
+
+class AuthenticatedKey(ID_BLAKE2b_256, RepoKey):
+    TYPE = 0x06
+    NAME = 'authenticated BLAKE2b'
+
+    def encrypt(self, chunk):
+        chunk = self.compress(chunk)
+        return b''.join([self.TYPE_STR, chunk.data])
+
+    def decrypt(self, id, data, decompress=True):
+        if data[0] != self.TYPE:
+            raise IntegrityError('Chunk %s: Invalid envelope' % bin_to_hex(id))
+        payload = memoryview(data)[1:]
+        if not decompress:
+            return Chunk(payload)
+        data = self.compressor.decompress(payload)
+        self.assert_id(id, data)
+        return Chunk(data)

+ 46 - 2
src/borg/testsuite/key.py

@@ -11,7 +11,8 @@ from ..helpers import Location
 from ..helpers import Chunk
 from ..helpers import IntegrityError
 from ..helpers import get_nonces_dir
-from ..key import PlaintextKey, PassphraseKey, KeyfileKey, Passphrase, PasswordRetriesExceeded, bin_to_hex
+from ..key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, AuthenticatedKey
+from ..key import Passphrase, PasswordRetriesExceeded, bin_to_hex
 
 
 class TestKey:
@@ -34,6 +35,24 @@ class TestKey:
         """))
     keyfile2_id = unhexlify('c3fbf14bc001ebcc3cd86e696c13482ed071740927cd7cbe1b01b4bfcee49314')
 
+    keyfile_blake2_key_file = """
+        BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000
+        hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaANAwo4EbUPF/kLQXhQnT4LxRc1advS8lUiegDa
+        q2Q6oOkP1Jc7MwBa7ZVMgoBG1sBeKYO6Sn6W6BBrHbMR8Dxv7xquaQIh8jIpnjLWpzyFIk
+        JlijFiTWI58Sxj+2D19b2ayFolnGkF9PJSARgfaieo0GkryqjcIgcXuKHO/H9NfaUDk5YJ
+        UqrJ9TUMohXSQzwF1pO4ak2BHPZKnbeJ7XL/8fFN8VFQZl27R0et4WlTFRBI1qQYyQaTiL
+        +/1ICMUpVsQM0mvyW6dc8/zGMsAlmZVApGhhc2jaACDdRF7uPv90UN3zsZy5Be89728RBl
+        zKvtzupDyTsfrJMqppdGVyYXRpb25zzgABhqCkc2FsdNoAIGTK3TR09UZqw1bPi17gyHOi
+        7YtSp4BVK7XptWeKh6Vip3ZlcnNpb24B""".strip()
+
+    keyfile_blake2_cdata = bytes.fromhex('04dd21cc91140ef009bc9e4dd634d075e39d39025ccce1289c'
+                                         '5536f9cb57f5f8130404040404040408ec852921309243b164')
+    # Verified against b2sum. Entire string passed to BLAKE2, including the 32 byte key contained in
+    # keyfile_blake2_key_file above is
+    # 037fb9b75b20d623f1d5a568050fccde4a1b7c5f5047432925e941a17c7a2d0d7061796c6f6164
+    #                                                                 p a y l o a d
+    keyfile_blake2_id = bytes.fromhex('a22d4fc81bb61c3846c334a09eaf28d22dd7df08c9a7a41e713ef28d80eebd45')
+
     @pytest.fixture
     def keys_dir(self, request, monkeypatch, tmpdir):
         monkeypatch.setenv('BORG_KEYS_DIR', tmpdir)
@@ -41,7 +60,11 @@ class TestKey:
 
     @pytest.fixture(params=(
         KeyfileKey,
-        PlaintextKey
+        PlaintextKey,
+        RepoKey,
+        Blake2KeyfileKey,
+        Blake2RepoKey,
+        AuthenticatedKey,
     ))
     def key(self, request, monkeypatch):
         monkeypatch.setenv('BORG_PASSPHRASE', 'test')
@@ -61,6 +84,12 @@ class TestKey:
         def commit_nonce_reservation(self, next_unreserved, start_nonce):
             pass
 
+        def save_key(self, data):
+            self.key_data = data
+
+        def load_key(self):
+            return self.key_data
+
     def test_plaintext(self):
         key = PlaintextKey.create(None, None)
         chunk = Chunk(b'foo')
@@ -128,6 +157,13 @@ class TestKey:
         key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
         assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata).data == b'payload'
 
+    def test_keyfile_blake2(self, monkeypatch, keys_dir):
+        with keys_dir.join('keyfile').open('w') as fd:
+            fd.write(self.keyfile_blake2_key_file)
+        monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase')
+        key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata)
+        assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata).data == b'payload'
+
     def test_passphrase(self, keys_dir, monkeypatch):
         monkeypatch.setenv('BORG_PASSPHRASE', 'test')
         key = PassphraseKey.create(self.MockRepository(), None)
@@ -193,6 +229,14 @@ class TestKey:
         with pytest.raises(IntegrityError):
             key.assert_id(id, plaintext_changed)
 
+    def test_authenticated_encrypt(self, monkeypatch):
+        monkeypatch.setenv('BORG_PASSPHRASE', 'test')
+        key = AuthenticatedKey.create(self.MockRepository(), self.MockArgs())
+        plaintext = Chunk(b'123456789')
+        authenticated = key.encrypt(plaintext)
+        # 0x06 is the key TYPE, 0x0000 identifies CNONE compression
+        assert authenticated == b'\x06\x00\x00' + plaintext.data
+
 
 class TestPassphrase:
     def test_passphrase_new_verification(self, capsys, monkeypatch):