1import configparser
2import getpass
3import os
4import shlex
5import sys
6import textwrap
7import subprocess
8from binascii import a2b_base64, b2a_base64, hexlify
9from hashlib import sha256, sha512, pbkdf2_hmac
10from hmac import HMAC, compare_digest
11
12from borg.logger import create_logger
13
14logger = create_logger()
15
16from ..constants import *  # NOQA
17from ..compress import Compressor
18from ..helpers import StableDict
19from ..helpers import Error, IntegrityError
20from ..helpers import yes
21from ..helpers import get_keys_dir, get_security_dir
22from ..helpers import get_limited_unpacker
23from ..helpers import bin_to_hex
24from ..helpers import prepare_subprocess_env
25from ..helpers import msgpack
26from ..item import Key, EncryptedKey
27from ..platform import SaveFile
28from .nonces import NonceManager
29from .low_level import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512
30
31PREFIX = b'\0' * 8
32
33
34class NoPassphraseFailure(Error):
35    """can not acquire a passphrase: {}"""
36
37
38class PassphraseWrong(Error):
39    """passphrase supplied in BORG_PASSPHRASE, by BORG_PASSCOMMAND or via BORG_PASSPHRASE_FD is incorrect."""
40
41
42class PasscommandFailure(Error):
43    """passcommand supplied in BORG_PASSCOMMAND failed: {}"""
44
45
46class PasswordRetriesExceeded(Error):
47    """exceeded the maximum password retries"""
48
49
50class UnsupportedPayloadError(Error):
51    """Unsupported payload type {}. A newer version is required to access this repository."""
52
53
54class UnsupportedManifestError(Error):
55    """Unsupported manifest envelope. A newer version is required to access this repository."""
56
57
58class KeyfileNotFoundError(Error):
59    """No key file for repository {} found in {}."""
60
61
62class KeyfileInvalidError(Error):
63    """Invalid key file for repository {} found in {}."""
64
65
66class KeyfileMismatchError(Error):
67    """Mismatch between repository {} and key file {}."""
68
69
70class RepoKeyNotFoundError(Error):
71    """No key entry found in the config of repository {}."""
72
73
74class TAMRequiredError(IntegrityError):
75    __doc__ = textwrap.dedent("""
76    Manifest is unauthenticated, but it is required for this repository.
77
78    This either means that you are under attack, or that you modified this repository
79    with a Borg version older than 1.0.9 after TAM authentication was enabled.
80
81    In the latter case, use "borg upgrade --tam --force '{}'" to re-authenticate the manifest.
82    """).strip()
83    traceback = False
84
85
86class TAMInvalid(IntegrityError):
87    __doc__ = IntegrityError.__doc__
88    traceback = False
89
90    def __init__(self):
91        # Error message becomes: "Data integrity error: Manifest authentication did not verify"
92        super().__init__('Manifest authentication did not verify')
93
94
95class TAMUnsupportedSuiteError(IntegrityError):
96    """Could not verify manifest: Unsupported suite {!r}; a newer version is needed."""
97    traceback = False
98
99
100class KeyBlobStorage:
101    NO_STORAGE = 'no_storage'
102    KEYFILE = 'keyfile'
103    REPO = 'repository'
104
105
106def key_creator(repository, args):
107    for key in AVAILABLE_KEY_TYPES:
108        if key.ARG_NAME == args.encryption:
109            assert key.ARG_NAME is not None
110            return key.create(repository, args)
111    else:
112        raise ValueError('Invalid encryption mode "%s"' % args.encryption)
113
114
115def key_argument_names():
116    return [key.ARG_NAME for key in AVAILABLE_KEY_TYPES if key.ARG_NAME]
117
118
119def identify_key(manifest_data):
120    key_type = manifest_data[0]
121    if key_type == PassphraseKey.TYPE:
122        # we just dispatch to repokey mode and assume the passphrase was migrated to a repokey.
123        # see also comment in PassphraseKey class.
124        return RepoKey
125
126    for key in AVAILABLE_KEY_TYPES:
127        if key.TYPE == key_type:
128            return key
129    else:
130        raise UnsupportedPayloadError(key_type)
131
132
133def key_factory(repository, manifest_data):
134    return identify_key(manifest_data).detect(repository, manifest_data)
135
136
137def tam_required_file(repository):
138    security_dir = get_security_dir(bin_to_hex(repository.id))
139    return os.path.join(security_dir, 'tam_required')
140
141
142def tam_required(repository):
143    file = tam_required_file(repository)
144    return os.path.isfile(file)
145
146
147class KeyBase:
148    # Numeric key type ID, must fit in one byte.
149    TYPE = None  # override in subclasses
150
151    # Human-readable name
152    NAME = 'UNDEFINED'
153
154    # Name used in command line / API (e.g. borg init --encryption=...)
155    ARG_NAME = 'UNDEFINED'
156
157    # Storage type (no key blob storage / keyfile / repo)
158    STORAGE = KeyBlobStorage.NO_STORAGE
159
160    # Seed for the buzhash chunker (borg.algorithms.chunker.Chunker)
161    # type: int
162    chunk_seed = None
163
164    # Whether this *particular instance* is encrypted from a practical point of view,
165    # i.e. when it's using encryption with a empty passphrase, then
166    # that may be *technically* called encryption, but for all intents and purposes
167    # that's as good as not encrypting in the first place, and this member should be False.
168    #
169    # The empty passphrase is also special because Borg tries it first when no passphrase
170    # was supplied, and if an empty passphrase works, then Borg won't ask for one.
171    logically_encrypted = False
172
173    def __init__(self, repository):
174        self.TYPE_STR = bytes([self.TYPE])
175        self.repository = repository
176        self.target = None  # key location file path / repo obj
177        # Some commands write new chunks (e.g. rename) but don't take a --compression argument. This duplicates
178        # the default used by those commands who do take a --compression argument.
179        self.compressor = Compressor('lz4')
180        self.decompress = self.compressor.decompress
181        self.tam_required = True
182
183    def id_hash(self, data):
184        """Return HMAC hash using the "id" HMAC key
185        """
186
187    def encrypt(self, chunk):
188        pass
189
190    def decrypt(self, id, data, decompress=True):
191        pass
192
193    def assert_id(self, id, data):
194        if id:
195            id_computed = self.id_hash(data)
196            if not compare_digest(id_computed, id):
197                raise IntegrityError('Chunk %s: id verification failed' % bin_to_hex(id))
198
199    def _tam_key(self, salt, context):
200        return hkdf_hmac_sha512(
201            ikm=self.id_key + self.enc_key + self.enc_hmac_key,
202            salt=salt,
203            info=b'borg-metadata-authentication-' + context,
204            output_length=64
205        )
206
207    def pack_and_authenticate_metadata(self, metadata_dict, context=b'manifest'):
208        metadata_dict = StableDict(metadata_dict)
209        tam = metadata_dict['tam'] = StableDict({
210            'type': 'HKDF_HMAC_SHA512',
211            'hmac': bytes(64),
212            'salt': os.urandom(64),
213        })
214        packed = msgpack.packb(metadata_dict, unicode_errors='surrogateescape')
215        tam_key = self._tam_key(tam['salt'], context)
216        tam['hmac'] = HMAC(tam_key, packed, sha512).digest()
217        return msgpack.packb(metadata_dict, unicode_errors='surrogateescape')
218
219    def unpack_and_verify_manifest(self, data, force_tam_not_required=False):
220        """Unpack msgpacked *data* and return (object, did_verify)."""
221        if data.startswith(b'\xc1' * 4):
222            # This is a manifest from the future, we can't read it.
223            raise UnsupportedManifestError()
224        tam_required = self.tam_required
225        if force_tam_not_required and tam_required:
226            logger.warning('Manifest authentication DISABLED.')
227            tam_required = False
228        data = bytearray(data)
229        unpacker = get_limited_unpacker('manifest')
230        unpacker.feed(data)
231        unpacked = unpacker.unpack()
232        if b'tam' not in unpacked:
233            if tam_required:
234                raise TAMRequiredError(self.repository._location.canonical_path())
235            else:
236                logger.debug('TAM not found and not required')
237                return unpacked, False
238        tam = unpacked.pop(b'tam', None)
239        if not isinstance(tam, dict):
240            raise TAMInvalid()
241        tam_type = tam.get(b'type', b'<none>').decode('ascii', 'replace')
242        if tam_type != 'HKDF_HMAC_SHA512':
243            if tam_required:
244                raise TAMUnsupportedSuiteError(repr(tam_type))
245            else:
246                logger.debug('Ignoring TAM made with unsupported suite, since TAM is not required: %r', tam_type)
247                return unpacked, False
248        tam_hmac = tam.get(b'hmac')
249        tam_salt = tam.get(b'salt')
250        if not isinstance(tam_salt, bytes) or not isinstance(tam_hmac, bytes):
251            raise TAMInvalid()
252        offset = data.index(tam_hmac)
253        data[offset:offset + 64] = bytes(64)
254        tam_key = self._tam_key(tam_salt, context=b'manifest')
255        calculated_hmac = HMAC(tam_key, data, sha512).digest()
256        if not compare_digest(calculated_hmac, tam_hmac):
257            raise TAMInvalid()
258        logger.debug('TAM-verified manifest')
259        return unpacked, True
260
261
262class PlaintextKey(KeyBase):
263    TYPE = 0x02
264    NAME = 'plaintext'
265    ARG_NAME = 'none'
266    STORAGE = KeyBlobStorage.NO_STORAGE
267
268    chunk_seed = 0
269    logically_encrypted = False
270
271    def __init__(self, repository):
272        super().__init__(repository)
273        self.tam_required = False
274
275    @classmethod
276    def create(cls, repository, args):
277        logger.info('Encryption NOT enabled.\nUse the "--encryption=repokey|keyfile" to enable encryption.')
278        return cls(repository)
279
280    @classmethod
281    def detect(cls, repository, manifest_data):
282        return cls(repository)
283
284    def id_hash(self, data):
285        return sha256(data).digest()
286
287    def encrypt(self, chunk):
288        data = self.compressor.compress(chunk)
289        return b''.join([self.TYPE_STR, data])
290
291    def decrypt(self, id, data, decompress=True):
292        if data[0] != self.TYPE:
293            id_str = bin_to_hex(id) if id is not None else '(unknown)'
294            raise IntegrityError('Chunk %s: Invalid encryption envelope' % id_str)
295        payload = memoryview(data)[1:]
296        if not decompress:
297            return payload
298        data = self.decompress(payload)
299        self.assert_id(id, data)
300        return data
301
302    def _tam_key(self, salt, context):
303        return salt + context
304
305
306def random_blake2b_256_key():
307    # This might look a bit curious, but is the same construction used in the keyed mode of BLAKE2b.
308    # Why limit the key to 64 bytes and pad it with 64 nulls nonetheless? The answer is that BLAKE2b
309    # has a 128 byte block size, but only 64 bytes of internal state (this is also referred to as a
310    # "local wide pipe" design, because the compression function transforms (block, state) => state,
311    # and len(block) >= len(state), hence wide.)
312    # In other words, a key longer than 64 bytes would have simply no advantage, since the function
313    # has no way of propagating more than 64 bytes of entropy internally.
314    # It's padded to a full block so that the key is never buffered internally by blake2b_update, ie.
315    # it remains in a single memory location that can be tracked and could be erased securely, if we
316    # wanted to.
317    return os.urandom(64) + bytes(64)
318
319
320class ID_BLAKE2b_256:
321    """
322    Key mix-in class for using BLAKE2b-256 for the id key.
323
324    The id_key length must be 32 bytes.
325    """
326
327    def id_hash(self, data):
328        return blake2b_256(self.id_key, data)
329
330    def init_from_random_data(self, data=None):
331        assert data is None  # PassphraseKey is the only caller using *data*
332        super().init_from_random_data()
333        self.enc_hmac_key = random_blake2b_256_key()
334        self.id_key = random_blake2b_256_key()
335
336
337class ID_HMAC_SHA_256:
338    """
339    Key mix-in class for using HMAC-SHA-256 for the id key.
340
341    The id_key length must be 32 bytes.
342    """
343
344    def id_hash(self, data):
345        return hmac_sha256(self.id_key, data)
346
347
348class AESKeyBase(KeyBase):
349    """
350    Common base class shared by KeyfileKey and PassphraseKey
351
352    Chunks are encrypted using 256bit AES in Counter Mode (CTR)
353
354    Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT
355
356    To reduce payload size only 8 bytes of the 16 bytes nonce is saved
357    in the payload, the first 8 bytes are always zeros. This does not
358    affect security but limits the maximum repository capacity to
359    only 295 exabytes!
360    """
361
362    PAYLOAD_OVERHEAD = 1 + 32 + 8  # TYPE + HMAC + NONCE
363
364    MAC = hmac_sha256
365
366    logically_encrypted = True
367
368    def encrypt(self, chunk):
369        data = self.compressor.compress(chunk)
370        self.nonce_manager.ensure_reservation(num_aes_blocks(len(data)))
371        self.enc_cipher.reset()
372        data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(data)))
373        assert (self.MAC is blake2b_256 and len(self.enc_hmac_key) == 128 or
374                self.MAC is hmac_sha256 and len(self.enc_hmac_key) == 32)
375        hmac = self.MAC(self.enc_hmac_key, data)
376        return b''.join((self.TYPE_STR, hmac, data))
377
378    def decrypt(self, id, data, decompress=True):
379        if not (data[0] == self.TYPE or
380            data[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)):
381            id_str = bin_to_hex(id) if id is not None else '(unknown)'
382            raise IntegrityError('Chunk %s: Invalid encryption envelope' % id_str)
383        data_view = memoryview(data)
384        hmac_given = data_view[1:33]
385        assert (self.MAC is blake2b_256 and len(self.enc_hmac_key) == 128 or
386                self.MAC is hmac_sha256 and len(self.enc_hmac_key) == 32)
387        hmac_computed = memoryview(self.MAC(self.enc_hmac_key, data_view[33:]))
388        if not compare_digest(hmac_computed, hmac_given):
389            id_str = bin_to_hex(id) if id is not None else '(unknown)'
390            raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % id_str)
391        self.dec_cipher.reset(iv=PREFIX + data[33:41])
392        payload = self.dec_cipher.decrypt(data_view[41:])
393        if not decompress:
394            return payload
395        data = self.decompress(payload)
396        self.assert_id(id, data)
397        return data
398
399    def extract_nonce(self, payload):
400        if not (payload[0] == self.TYPE or
401            payload[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)):
402            raise IntegrityError('Manifest: Invalid encryption envelope')
403        nonce = bytes_to_long(payload[33:41])
404        return nonce
405
406    def init_from_random_data(self, data=None):
407        if data is None:
408            data = os.urandom(100)
409        self.enc_key = data[0:32]
410        self.enc_hmac_key = data[32:64]
411        self.id_key = data[64:96]
412        self.chunk_seed = bytes_to_int(data[96:100])
413        # Convert to signed int32
414        if self.chunk_seed & 0x80000000:
415            self.chunk_seed = self.chunk_seed - 0xffffffff - 1
416
417    def init_ciphers(self, manifest_nonce=0):
418        self.enc_cipher = AES(is_encrypt=True, key=self.enc_key, iv=manifest_nonce.to_bytes(16, byteorder='big'))
419        self.nonce_manager = NonceManager(self.repository, self.enc_cipher, manifest_nonce)
420        self.dec_cipher = AES(is_encrypt=False, key=self.enc_key)
421
422
423class Passphrase(str):
424    @classmethod
425    def _env_passphrase(cls, env_var, default=None):
426        passphrase = os.environ.get(env_var, default)
427        if passphrase is not None:
428            return cls(passphrase)
429
430    @classmethod
431    def env_passphrase(cls, default=None):
432        passphrase = cls._env_passphrase('BORG_PASSPHRASE', default)
433        if passphrase is not None:
434            return passphrase
435        passphrase = cls.env_passcommand()
436        if passphrase is not None:
437            return passphrase
438        passphrase = cls.fd_passphrase()
439        if passphrase is not None:
440            return passphrase
441
442    @classmethod
443    def env_passcommand(cls, default=None):
444        passcommand = os.environ.get('BORG_PASSCOMMAND', None)
445        if passcommand is not None:
446            # passcommand is a system command (not inside pyinstaller env)
447            env = prepare_subprocess_env(system=True)
448            try:
449                passphrase = subprocess.check_output(shlex.split(passcommand), universal_newlines=True, env=env)
450            except (subprocess.CalledProcessError, FileNotFoundError) as e:
451                raise PasscommandFailure(e)
452            return cls(passphrase.rstrip('\n'))
453
454    @classmethod
455    def fd_passphrase(cls):
456        try:
457            fd = int(os.environ.get('BORG_PASSPHRASE_FD'))
458        except (ValueError, TypeError):
459            return None
460        with os.fdopen(fd, mode='r') as f:
461            passphrase = f.read()
462        return cls(passphrase.rstrip('\n'))
463
464    @classmethod
465    def env_new_passphrase(cls, default=None):
466        return cls._env_passphrase('BORG_NEW_PASSPHRASE', default)
467
468    @classmethod
469    def getpass(cls, prompt):
470        try:
471            pw = getpass.getpass(prompt)
472        except EOFError:
473            if prompt:
474                print()  # avoid err msg appearing right of prompt
475            msg = []
476            for env_var in 'BORG_PASSPHRASE', 'BORG_PASSCOMMAND':
477                env_var_set = os.environ.get(env_var) is not None
478                msg.append('%s is %s.' % (env_var, 'set' if env_var_set else 'not set'))
479            msg.append('Interactive password query failed.')
480            raise NoPassphraseFailure(' '.join(msg)) from None
481        else:
482            return cls(pw)
483
484    @classmethod
485    def verification(cls, passphrase):
486        msg = 'Do you want your passphrase to be displayed for verification? [yN]: '
487        if yes(msg, retry_msg=msg, invalid_msg='Invalid answer, try again.',
488               retry=True, env_var_override='BORG_DISPLAY_PASSPHRASE'):
489            print('Your passphrase (between double-quotes): "%s"' % passphrase,
490                  file=sys.stderr)
491            print('Make sure the passphrase displayed above is exactly what you wanted.',
492                  file=sys.stderr)
493            try:
494                passphrase.encode('ascii')
495            except UnicodeEncodeError:
496                print('Your passphrase (UTF-8 encoding in hex): %s' %
497                      bin_to_hex(passphrase.encode('utf-8')),
498                      file=sys.stderr)
499                print('As you have a non-ASCII passphrase, it is recommended to keep the UTF-8 encoding in hex together with the passphrase at a safe place.',
500                      file=sys.stderr)
501
502    @classmethod
503    def new(cls, allow_empty=False):
504        passphrase = cls.env_new_passphrase()
505        if passphrase is not None:
506            return passphrase
507        passphrase = cls.env_passphrase()
508        if passphrase is not None:
509            return passphrase
510        for retry in range(1, 11):
511            passphrase = cls.getpass('Enter new passphrase: ')
512            if allow_empty or passphrase:
513                passphrase2 = cls.getpass('Enter same passphrase again: ')
514                if passphrase == passphrase2:
515                    cls.verification(passphrase)
516                    logger.info('Remember your passphrase. Your data will be inaccessible without it.')
517                    return passphrase
518                else:
519                    print('Passphrases do not match', file=sys.stderr)
520            else:
521                print('Passphrase must not be blank', file=sys.stderr)
522        else:
523            raise PasswordRetriesExceeded
524
525    def __repr__(self):
526        return '<Passphrase "***hidden***">'
527
528    def kdf(self, salt, iterations, length):
529        return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
530
531
532class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase):
533    # This mode was killed in borg 1.0, see: https://github.com/borgbackup/borg/issues/97
534    # Reasons:
535    # - you can never ever change your passphrase for existing repos.
536    # - you can never ever use a different iterations count for existing repos.
537    # "Killed" means:
538    # - there is no automatic dispatch to this class via type byte
539    # - --encryption=passphrase is an invalid argument now
540    # This class is kept for a while to support migration from passphrase to repokey mode.
541    TYPE = 0x01
542    NAME = 'passphrase'
543    ARG_NAME = None
544    STORAGE = KeyBlobStorage.NO_STORAGE
545
546    iterations = 100000  # must not be changed ever!
547
548    @classmethod
549    def create(cls, repository, args):
550        key = cls(repository)
551        logger.warning('WARNING: "passphrase" mode is unsupported since borg 1.0.')
552        passphrase = Passphrase.new(allow_empty=False)
553        key.init(repository, passphrase)
554        return key
555
556    @classmethod
557    def detect(cls, repository, manifest_data):
558        prompt = 'Enter passphrase for %s: ' % repository._location.canonical_path()
559        key = cls(repository)
560        passphrase = Passphrase.env_passphrase()
561        if passphrase is None:
562            passphrase = Passphrase.getpass(prompt)
563        for retry in range(1, 3):
564            key.init(repository, passphrase)
565            try:
566                key.decrypt(None, manifest_data)
567                num_blocks = num_aes_blocks(len(manifest_data) - 41)
568                key.init_ciphers(key.extract_nonce(manifest_data) + num_blocks)
569                key._passphrase = passphrase
570                return key
571            except IntegrityError:
572                passphrase = Passphrase.getpass(prompt)
573        else:
574            raise PasswordRetriesExceeded
575
576    def change_passphrase(self):
577        class ImmutablePassphraseError(Error):
578            """The passphrase for this encryption key type can't be changed."""
579
580        raise ImmutablePassphraseError
581
582    def init(self, repository, passphrase):
583        self.init_from_random_data(passphrase.kdf(repository.id, self.iterations, 100))
584        self.init_ciphers()
585        self.tam_required = False
586
587
588class KeyfileKeyBase(AESKeyBase):
589    @classmethod
590    def detect(cls, repository, manifest_data):
591        key = cls(repository)
592        target = key.find_key()
593        prompt = 'Enter passphrase for key %s: ' % target
594        passphrase = Passphrase.env_passphrase()
595        if passphrase is None:
596            passphrase = Passphrase()
597            if not key.load(target, passphrase):
598                for retry in range(0, 3):
599                    passphrase = Passphrase.getpass(prompt)
600                    if key.load(target, passphrase):
601                        break
602                else:
603                    raise PasswordRetriesExceeded
604        else:
605            if not key.load(target, passphrase):
606                raise PassphraseWrong
607        num_blocks = num_aes_blocks(len(manifest_data) - 41)
608        key.init_ciphers(key.extract_nonce(manifest_data) + num_blocks)
609        key._passphrase = passphrase
610        return key
611
612    def find_key(self):
613        raise NotImplementedError
614
615    def load(self, target, passphrase):
616        raise NotImplementedError
617
618    def _load(self, key_data, passphrase):
619        cdata = a2b_base64(key_data)
620        data = self.decrypt_key_file(cdata, passphrase)
621        if data:
622            data = msgpack.unpackb(data)
623            key = Key(internal_dict=data)
624            if key.version != 1:
625                raise IntegrityError('Invalid key file header')
626            self.repository_id = key.repository_id
627            self.enc_key = key.enc_key
628            self.enc_hmac_key = key.enc_hmac_key
629            self.id_key = key.id_key
630            self.chunk_seed = key.chunk_seed
631            self.tam_required = key.get('tam_required', tam_required(self.repository))
632            return True
633        return False
634
635    def decrypt_key_file(self, data, passphrase):
636        unpacker = get_limited_unpacker('key')
637        unpacker.feed(data)
638        data = unpacker.unpack()
639        enc_key = EncryptedKey(internal_dict=data)
640        assert enc_key.version == 1
641        assert enc_key.algorithm == 'sha256'
642        key = passphrase.kdf(enc_key.salt, enc_key.iterations, 32)
643        data = AES(is_encrypt=False, key=key).decrypt(enc_key.data)
644        if hmac_sha256(key, data) == enc_key.hash:
645            return data
646
647    def encrypt_key_file(self, data, passphrase):
648        salt = os.urandom(32)
649        iterations = PBKDF2_ITERATIONS
650        key = passphrase.kdf(salt, iterations, 32)
651        hash = hmac_sha256(key, data)
652        cdata = AES(is_encrypt=True, key=key).encrypt(data)
653        enc_key = EncryptedKey(
654            version=1,
655            salt=salt,
656            iterations=iterations,
657            algorithm='sha256',
658            hash=hash,
659            data=cdata,
660        )
661        return msgpack.packb(enc_key.as_dict())
662
663    def _save(self, passphrase):
664        key = Key(
665            version=1,
666            repository_id=self.repository_id,
667            enc_key=self.enc_key,
668            enc_hmac_key=self.enc_hmac_key,
669            id_key=self.id_key,
670            chunk_seed=self.chunk_seed,
671            tam_required=self.tam_required,
672        )
673        data = self.encrypt_key_file(msgpack.packb(key.as_dict()), passphrase)
674        key_data = '\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii')))
675        return key_data
676
677    def change_passphrase(self, passphrase=None):
678        if passphrase is None:
679            passphrase = Passphrase.new(allow_empty=True)
680        self.save(self.target, passphrase)
681
682    @classmethod
683    def create(cls, repository, args):
684        passphrase = Passphrase.new(allow_empty=True)
685        key = cls(repository)
686        key.repository_id = repository.id
687        key.init_from_random_data()
688        key.init_ciphers()
689        target = key.get_new_target(args)
690        key.save(target, passphrase)
691        logger.info('Key in "%s" created.' % target)
692        logger.info('Keep this key safe. Your data will be inaccessible without it.')
693        return key
694
695    def save(self, target, passphrase):
696        raise NotImplementedError
697
698    def get_new_target(self, args):
699        raise NotImplementedError
700
701
702class KeyfileKey(ID_HMAC_SHA_256, KeyfileKeyBase):
703    TYPE = 0x00
704    NAME = 'key file'
705    ARG_NAME = 'keyfile'
706    STORAGE = KeyBlobStorage.KEYFILE
707
708    FILE_ID = 'BORG_KEY'
709
710    def sanity_check(self, filename, id):
711        file_id = self.FILE_ID.encode() + b' '
712        repo_id = hexlify(id)
713        with open(filename, 'rb') as fd:
714            # we do the magic / id check in binary mode to avoid stumbling over
715            # decoding errors if somebody has binary files in the keys dir for some reason.
716            if fd.read(len(file_id)) != file_id:
717                raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)
718            if fd.read(len(repo_id)) != repo_id:
719                raise KeyfileMismatchError(self.repository._location.canonical_path(), filename)
720            return filename
721
722    def find_key(self):
723        id = self.repository.id
724        keyfile = os.environ.get('BORG_KEY_FILE')
725        if keyfile:
726            return self.sanity_check(os.path.abspath(keyfile), id)
727        keys_dir = get_keys_dir()
728        for name in os.listdir(keys_dir):
729            filename = os.path.join(keys_dir, name)
730            try:
731                return self.sanity_check(filename, id)
732            except (KeyfileInvalidError, KeyfileMismatchError):
733                pass
734        raise KeyfileNotFoundError(self.repository._location.canonical_path(), get_keys_dir())
735
736    def get_new_target(self, args):
737        keyfile = os.environ.get('BORG_KEY_FILE')
738        if keyfile:
739            return os.path.abspath(keyfile)
740        filename = args.location.to_key_filename()
741        path = filename
742        i = 1
743        while os.path.exists(path):
744            i += 1
745            path = filename + '.%d' % i
746        return path
747
748    def load(self, target, passphrase):
749        with open(target, 'r') as fd:
750            key_data = ''.join(fd.readlines()[1:])
751        success = self._load(key_data, passphrase)
752        if success:
753            self.target = target
754        return success
755
756    def save(self, target, passphrase):
757        key_data = self._save(passphrase)
758        with SaveFile(target) as fd:
759            fd.write('%s %s\n' % (self.FILE_ID, bin_to_hex(self.repository_id)))
760            fd.write(key_data)
761            fd.write('\n')
762        self.target = target
763
764
765class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase):
766    TYPE = 0x03
767    NAME = 'repokey'
768    ARG_NAME = 'repokey'
769    STORAGE = KeyBlobStorage.REPO
770
771    def find_key(self):
772        loc = self.repository._location.canonical_path()
773        try:
774            self.repository.load_key()
775            return loc
776        except configparser.NoOptionError:
777            raise RepoKeyNotFoundError(loc) from None
778
779    def get_new_target(self, args):
780        return self.repository
781
782    def load(self, target, passphrase):
783        # While the repository is encrypted, we consider a repokey repository with a blank
784        # passphrase an unencrypted repository.
785        self.logically_encrypted = passphrase != ''
786
787        # what we get in target is just a repo location, but we already have the repo obj:
788        target = self.repository
789        key_data = target.load_key()
790        key_data = key_data.decode('utf-8')  # remote repo: msgpack issue #99, getting bytes
791        success = self._load(key_data, passphrase)
792        if success:
793            self.target = target
794        return success
795
796    def save(self, target, passphrase):
797        self.logically_encrypted = passphrase != ''
798        key_data = self._save(passphrase)
799        key_data = key_data.encode('utf-8')  # remote repo: msgpack issue #99, giving bytes
800        target.save_key(key_data)
801        self.target = target
802
803
804class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey):
805    TYPE = 0x04
806    NAME = 'key file BLAKE2b'
807    ARG_NAME = 'keyfile-blake2'
808    STORAGE = KeyBlobStorage.KEYFILE
809
810    FILE_ID = 'BORG_KEY'
811    MAC = blake2b_256
812
813
814class Blake2RepoKey(ID_BLAKE2b_256, RepoKey):
815    TYPE = 0x05
816    NAME = 'repokey BLAKE2b'
817    ARG_NAME = 'repokey-blake2'
818    STORAGE = KeyBlobStorage.REPO
819
820    MAC = blake2b_256
821
822
823class AuthenticatedKeyBase(RepoKey):
824    STORAGE = KeyBlobStorage.REPO
825
826    # It's only authenticated, not encrypted.
827    logically_encrypted = False
828
829    def load(self, target, passphrase):
830        success = super().load(target, passphrase)
831        self.logically_encrypted = False
832        return success
833
834    def save(self, target, passphrase):
835        super().save(target, passphrase)
836        self.logically_encrypted = False
837
838    def extract_nonce(self, payload):
839        # This is called during set-up of the AES ciphers we're not actually using for this
840        # key. Therefore the return value of this method doesn't matter; it's just around
841        # to not have it crash should key identification be run against a very small chunk
842        # by "borg check" when the manifest is lost. (The manifest is always large enough
843        # to have the original method read some garbage from bytes 33-41). (Also, the return
844        # value must be larger than the 41 byte bloat of the original format).
845        if payload[0] != self.TYPE:
846            raise IntegrityError('Manifest: Invalid encryption envelope')
847        return 42
848
849    def encrypt(self, chunk):
850        data = self.compressor.compress(chunk)
851        return b''.join([self.TYPE_STR, data])
852
853    def decrypt(self, id, data, decompress=True):
854        if data[0] != self.TYPE:
855            raise IntegrityError('Chunk %s: Invalid envelope' % bin_to_hex(id))
856        payload = memoryview(data)[1:]
857        if not decompress:
858            return payload
859        data = self.decompress(payload)
860        self.assert_id(id, data)
861        return data
862
863
864class AuthenticatedKey(AuthenticatedKeyBase):
865    TYPE = 0x07
866    NAME = 'authenticated'
867    ARG_NAME = 'authenticated'
868
869
870class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase):
871    TYPE = 0x06
872    NAME = 'authenticated BLAKE2b'
873    ARG_NAME = 'authenticated-blake2'
874
875
876AVAILABLE_KEY_TYPES = (
877    PlaintextKey,
878    PassphraseKey,
879    KeyfileKey, RepoKey, AuthenticatedKey,
880    Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey,
881)
882