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