key.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. from binascii import hexlify, a2b_base64, b2a_base64
  2. import configparser
  3. import getpass
  4. import os
  5. import shlex
  6. import subprocess
  7. import sys
  8. import textwrap
  9. from hmac import HMAC, compare_digest
  10. from hashlib import sha256, sha512, pbkdf2_hmac
  11. import msgpack
  12. from .helpers import StableDict, IntegrityError, get_keys_dir, get_security_dir, Error, yes, bin_to_hex
  13. from .logger import create_logger
  14. logger = create_logger()
  15. from .crypto import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks
  16. from .crypto import hkdf_hmac_sha512
  17. from .compress import Compressor, CNONE
  18. from .helpers import get_limited_unpacker
  19. from .helpers import prepare_subprocess_env
  20. PREFIX = b'\0' * 8
  21. class PassphraseWrong(Error):
  22. """passphrase supplied in BORG_PASSPHRASE or by BORG_PASSCOMMAND is incorrect."""
  23. class PasscommandFailure(Error):
  24. """passcommand supplied in BORG_PASSCOMMAND failed: {}"""
  25. class PasswordRetriesExceeded(Error):
  26. """exceeded the maximum password retries"""
  27. class UnsupportedPayloadError(Error):
  28. """Unsupported payload type {}. A newer version is required to access this repository."""
  29. class UnsupportedManifestError(Error):
  30. """Unsupported manifest envelope. A newer version is required to access this repository."""
  31. class KeyfileNotFoundError(Error):
  32. """No key file for repository {} found in {}."""
  33. class RepoKeyNotFoundError(Error):
  34. """No key entry found in the config of repository {}."""
  35. class TAMRequiredError(IntegrityError):
  36. __doc__ = textwrap.dedent("""
  37. Manifest is unauthenticated, but it is required for this repository.
  38. This either means that you are under attack, or that you modified this repository
  39. with a Borg version older than 1.0.9 after TAM authentication was enabled.
  40. In the latter case, use "borg upgrade --tam --force '{}'" to re-authenticate the manifest.
  41. """).strip()
  42. traceback = False
  43. class TAMInvalid(IntegrityError):
  44. __doc__ = IntegrityError.__doc__
  45. traceback = False
  46. def __init__(self):
  47. # Error message becomes: "Data integrity error: Manifest authentication did not verify"
  48. super().__init__('Manifest authentication did not verify')
  49. class TAMUnsupportedSuiteError(IntegrityError):
  50. """Could not verify manifest: Unsupported suite {!r}; a newer version is needed."""
  51. traceback = False
  52. def key_creator(repository, args):
  53. if args.encryption == 'keyfile':
  54. return KeyfileKey.create(repository, args)
  55. elif args.encryption == 'repokey':
  56. return RepoKey.create(repository, args)
  57. else:
  58. return PlaintextKey.create(repository, args)
  59. def key_factory(repository, manifest_data):
  60. key_type = manifest_data[0]
  61. if key_type == KeyfileKey.TYPE:
  62. return KeyfileKey.detect(repository, manifest_data)
  63. elif key_type == RepoKey.TYPE:
  64. return RepoKey.detect(repository, manifest_data)
  65. elif key_type == PassphraseKey.TYPE:
  66. # we just dispatch to repokey mode and assume the passphrase was migrated to a repokey.
  67. # see also comment in PassphraseKey class.
  68. return RepoKey.detect(repository, manifest_data)
  69. elif key_type == PlaintextKey.TYPE:
  70. return PlaintextKey.detect(repository, manifest_data)
  71. else:
  72. raise UnsupportedPayloadError(key_type)
  73. def tam_required_file(repository):
  74. security_dir = get_security_dir(bin_to_hex(repository.id))
  75. return os.path.join(security_dir, 'tam_required')
  76. def tam_required(repository):
  77. file = tam_required_file(repository)
  78. return os.path.isfile(file)
  79. class KeyBase:
  80. TYPE = None # override in subclasses
  81. def __init__(self, repository):
  82. self.TYPE_STR = bytes([self.TYPE])
  83. self.repository = repository
  84. self.target = None # key location file path / repo obj
  85. self.compressor = Compressor('none')
  86. self.tam_required = True
  87. def id_hash(self, data):
  88. """Return HMAC hash using the "id" HMAC key
  89. """
  90. def encrypt(self, data, none_compression=False):
  91. pass
  92. def decrypt(self, id, data):
  93. pass
  94. def _tam_key(self, salt, context):
  95. return hkdf_hmac_sha512(
  96. ikm=self.id_key + self.enc_key + self.enc_hmac_key,
  97. salt=salt,
  98. info=b'borg-metadata-authentication-' + context,
  99. output_length=64
  100. )
  101. def pack_and_authenticate_metadata(self, metadata_dict, context=b'manifest'):
  102. metadata_dict = StableDict(metadata_dict)
  103. tam = metadata_dict['tam'] = StableDict({
  104. 'type': 'HKDF_HMAC_SHA512',
  105. 'hmac': bytes(64),
  106. 'salt': os.urandom(64),
  107. })
  108. packed = msgpack.packb(metadata_dict, unicode_errors='surrogateescape')
  109. tam_key = self._tam_key(tam['salt'], context)
  110. tam['hmac'] = HMAC(tam_key, packed, sha512).digest()
  111. return msgpack.packb(metadata_dict, unicode_errors='surrogateescape')
  112. def unpack_and_verify_manifest(self, data, force_tam_not_required=False):
  113. """Unpack msgpacked *data* and return (object, did_verify)."""
  114. if data.startswith(b'\xc1' * 4):
  115. # This is a manifest from the future, we can't read it.
  116. raise UnsupportedManifestError()
  117. tam_required = self.tam_required
  118. if force_tam_not_required and tam_required:
  119. logger.warning('Manifest authentication DISABLED.')
  120. tam_required = False
  121. data = bytearray(data)
  122. unpacker = get_limited_unpacker('manifest')
  123. unpacker.feed(data)
  124. unpacked = unpacker.unpack()
  125. if b'tam' not in unpacked:
  126. if tam_required:
  127. raise TAMRequiredError(self.repository._location.canonical_path())
  128. else:
  129. logger.debug('TAM not found and not required')
  130. return unpacked, False
  131. tam = unpacked.pop(b'tam', None)
  132. if not isinstance(tam, dict):
  133. raise TAMInvalid()
  134. tam_type = tam.get(b'type', b'<none>').decode('ascii', 'replace')
  135. if tam_type != 'HKDF_HMAC_SHA512':
  136. if tam_required:
  137. raise TAMUnsupportedSuiteError(repr(tam_type))
  138. else:
  139. logger.debug('Ignoring TAM made with unsupported suite, since TAM is not required: %r', tam_type)
  140. return unpacked, False
  141. tam_hmac = tam.get(b'hmac')
  142. tam_salt = tam.get(b'salt')
  143. if not isinstance(tam_salt, bytes) or not isinstance(tam_hmac, bytes):
  144. raise TAMInvalid()
  145. offset = data.index(tam_hmac)
  146. data[offset:offset + 64] = bytes(64)
  147. tam_key = self._tam_key(tam_salt, context=b'manifest')
  148. calculated_hmac = HMAC(tam_key, data, sha512).digest()
  149. if not compare_digest(calculated_hmac, tam_hmac):
  150. raise TAMInvalid()
  151. logger.debug('TAM-verified manifest')
  152. return unpacked, True
  153. class PlaintextKey(KeyBase):
  154. TYPE = 0x02
  155. chunk_seed = 0
  156. def __init__(self, repository):
  157. super().__init__(repository)
  158. self.tam_required = False
  159. @classmethod
  160. def create(cls, repository, args):
  161. logger.info('Encryption NOT enabled.\nUse the "--encryption=repokey|keyfile" to enable encryption.')
  162. return cls(repository)
  163. @classmethod
  164. def detect(cls, repository, manifest_data):
  165. return cls(repository)
  166. def id_hash(self, data):
  167. return sha256(data).digest()
  168. def encrypt(self, data, none_compression=False):
  169. if none_compression:
  170. compressed = CNONE().compress(data)
  171. else:
  172. compressed = self.compressor.compress(data)
  173. return b''.join([self.TYPE_STR, compressed])
  174. def decrypt(self, id, data):
  175. if data[0] != self.TYPE:
  176. id_str = bin_to_hex(id) if id is not None else '(unknown)'
  177. raise IntegrityError('Chunk %s: Invalid encryption envelope' % id_str)
  178. data = self.compressor.decompress(memoryview(data)[1:])
  179. if id and sha256(data).digest() != id:
  180. raise IntegrityError('Chunk %s: id verification failed' % bin_to_hex(id))
  181. return data
  182. def _tam_key(self, salt, context):
  183. return salt + context
  184. class AESKeyBase(KeyBase):
  185. """Common base class shared by KeyfileKey and PassphraseKey
  186. Chunks are encrypted using 256bit AES in Counter Mode (CTR)
  187. Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT
  188. To reduce payload size only 8 bytes of the 16 bytes nonce is saved
  189. in the payload, the first 8 bytes are always zeros. This does not
  190. affect security but limits the maximum repository capacity to
  191. only 295 exabytes!
  192. """
  193. PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE
  194. def id_hash(self, data):
  195. """Return HMAC hash using the "id" HMAC key
  196. """
  197. return HMAC(self.id_key, data, sha256).digest()
  198. def encrypt(self, data, none_compression=False):
  199. if none_compression:
  200. data = CNONE().compress(data)
  201. else:
  202. data = self.compressor.compress(data)
  203. self.enc_cipher.reset()
  204. data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(data)))
  205. hmac = HMAC(self.enc_hmac_key, data, sha256).digest()
  206. return b''.join((self.TYPE_STR, hmac, data))
  207. def decrypt(self, id, data):
  208. if not (data[0] == self.TYPE or
  209. data[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)):
  210. id_str = bin_to_hex(id) if id is not None else '(unknown)'
  211. raise IntegrityError('Chunk %s: Invalid encryption envelope' % id_str)
  212. hmac_given = memoryview(data)[1:33]
  213. hmac_computed = memoryview(HMAC(self.enc_hmac_key, memoryview(data)[33:], sha256).digest())
  214. if not compare_digest(hmac_computed, hmac_given):
  215. id_str = bin_to_hex(id) if id is not None else '(unknown)'
  216. raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % id_str)
  217. self.dec_cipher.reset(iv=PREFIX + data[33:41])
  218. data = self.compressor.decompress(self.dec_cipher.decrypt(data[41:]))
  219. if id:
  220. hmac_given = id
  221. hmac_computed = HMAC(self.id_key, data, sha256).digest()
  222. if not compare_digest(hmac_computed, hmac_given):
  223. raise IntegrityError('Chunk %s: Chunk id verification failed' % bin_to_hex(id))
  224. return data
  225. def extract_nonce(self, payload):
  226. if not (payload[0] == self.TYPE or
  227. payload[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)):
  228. raise IntegrityError('Manifest: Invalid encryption envelope')
  229. nonce = bytes_to_long(payload[33:41])
  230. return nonce
  231. def init_from_random_data(self, data):
  232. self.enc_key = data[0:32]
  233. self.enc_hmac_key = data[32:64]
  234. self.id_key = data[64:96]
  235. self.chunk_seed = bytes_to_int(data[96:100])
  236. # Convert to signed int32
  237. if self.chunk_seed & 0x80000000:
  238. self.chunk_seed = self.chunk_seed - 0xffffffff - 1
  239. def init_ciphers(self, enc_iv=b''):
  240. self.enc_cipher = AES(is_encrypt=True, key=self.enc_key, iv=enc_iv)
  241. self.dec_cipher = AES(is_encrypt=False, key=self.enc_key)
  242. class Passphrase(str):
  243. @classmethod
  244. def _env_passphrase(cls, env_var, default=None):
  245. passphrase = os.environ.get(env_var, default)
  246. if passphrase is not None:
  247. return cls(passphrase)
  248. @classmethod
  249. def env_passphrase(cls, default=None):
  250. passphrase = cls._env_passphrase('BORG_PASSPHRASE', default)
  251. if passphrase is not None:
  252. return passphrase
  253. passphrase = cls.env_passcommand()
  254. if passphrase is not None:
  255. return passphrase
  256. @classmethod
  257. def env_passcommand(cls, default=None):
  258. passcommand = os.environ.get('BORG_PASSCOMMAND', None)
  259. if passcommand is not None:
  260. # passcommand is a system command (not inside pyinstaller env)
  261. env = prepare_subprocess_env(system=True)
  262. try:
  263. passphrase = subprocess.check_output(shlex.split(passcommand), universal_newlines=True, env=env)
  264. except (subprocess.CalledProcessError, FileNotFoundError) as e:
  265. raise PasscommandFailure(e)
  266. return cls(passphrase.rstrip('\n'))
  267. @classmethod
  268. def getpass(cls, prompt):
  269. return cls(getpass.getpass(prompt))
  270. @classmethod
  271. def verification(cls, passphrase):
  272. msg = 'Do you want your passphrase to be displayed for verification? [yN]: '
  273. if yes(msg, retry_msg=msg, invalid_msg='Invalid answer, try again.',
  274. retry=True, env_var_override='BORG_DISPLAY_PASSPHRASE'):
  275. print('Your passphrase (between double-quotes): "%s"' % passphrase,
  276. file=sys.stderr)
  277. print('Make sure the passphrase displayed above is exactly what you wanted.',
  278. file=sys.stderr)
  279. try:
  280. passphrase.encode('ascii')
  281. except UnicodeEncodeError:
  282. print('Your passphrase (UTF-8 encoding in hex): %s' %
  283. bin_to_hex(passphrase.encode('utf-8')),
  284. file=sys.stderr)
  285. 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.',
  286. file=sys.stderr)
  287. @classmethod
  288. def new(cls, allow_empty=False):
  289. passphrase = cls.env_passphrase()
  290. if passphrase is not None:
  291. return passphrase
  292. for retry in range(1, 11):
  293. passphrase = cls.getpass('Enter new passphrase: ')
  294. if allow_empty or passphrase:
  295. passphrase2 = cls.getpass('Enter same passphrase again: ')
  296. if passphrase == passphrase2:
  297. cls.verification(passphrase)
  298. logger.info('Remember your passphrase. Your data will be inaccessible without it.')
  299. return passphrase
  300. else:
  301. print('Passphrases do not match', file=sys.stderr)
  302. else:
  303. print('Passphrase must not be blank', file=sys.stderr)
  304. else:
  305. raise PasswordRetriesExceeded
  306. def __repr__(self):
  307. return '<Passphrase "***hidden***">'
  308. def kdf(self, salt, iterations, length):
  309. return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
  310. class PassphraseKey(AESKeyBase):
  311. # This mode was killed in borg 1.0, see: https://github.com/borgbackup/borg/issues/97
  312. # Reasons:
  313. # - you can never ever change your passphrase for existing repos.
  314. # - you can never ever use a different iterations count for existing repos.
  315. # "Killed" means:
  316. # - there is no automatic dispatch to this class via type byte
  317. # - --encryption=passphrase is an invalid argument now
  318. # This class is kept for a while to support migration from passphrase to repokey mode.
  319. TYPE = 0x01
  320. iterations = 100000 # must not be changed ever!
  321. @classmethod
  322. def create(cls, repository, args):
  323. key = cls(repository)
  324. logger.warning('WARNING: "passphrase" mode is unsupported since borg 1.0.')
  325. passphrase = Passphrase.new(allow_empty=False)
  326. key.init(repository, passphrase)
  327. return key
  328. @classmethod
  329. def detect(cls, repository, manifest_data):
  330. prompt = 'Enter passphrase for %s: ' % repository._location.orig
  331. key = cls(repository)
  332. passphrase = Passphrase.env_passphrase()
  333. if passphrase is None:
  334. passphrase = Passphrase.getpass(prompt)
  335. for retry in range(1, 3):
  336. key.init(repository, passphrase)
  337. try:
  338. key.decrypt(None, manifest_data)
  339. num_blocks = num_aes_blocks(len(manifest_data) - 41)
  340. key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks))
  341. key._passphrase = passphrase
  342. return key
  343. except IntegrityError:
  344. passphrase = Passphrase.getpass(prompt)
  345. else:
  346. raise PasswordRetriesExceeded
  347. def change_passphrase(self):
  348. class ImmutablePassphraseError(Error):
  349. """The passphrase for this encryption key type can't be changed."""
  350. raise ImmutablePassphraseError
  351. def init(self, repository, passphrase):
  352. self.init_from_random_data(passphrase.kdf(repository.id, self.iterations, 100))
  353. self.init_ciphers()
  354. self.tam_required = False
  355. class KeyfileKeyBase(AESKeyBase):
  356. @classmethod
  357. def detect(cls, repository, manifest_data):
  358. key = cls(repository)
  359. target = key.find_key()
  360. prompt = 'Enter passphrase for key %s: ' % target
  361. passphrase = Passphrase.env_passphrase()
  362. if passphrase is None:
  363. passphrase = Passphrase()
  364. if not key.load(target, passphrase):
  365. for retry in range(0, 3):
  366. passphrase = Passphrase.getpass(prompt)
  367. if key.load(target, passphrase):
  368. break
  369. else:
  370. raise PasswordRetriesExceeded
  371. else:
  372. if not key.load(target, passphrase):
  373. raise PassphraseWrong
  374. num_blocks = num_aes_blocks(len(manifest_data) - 41)
  375. key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks))
  376. key._passphrase = passphrase
  377. return key
  378. def find_key(self):
  379. raise NotImplementedError
  380. def load(self, target, passphrase):
  381. raise NotImplementedError
  382. def _load(self, key_data, passphrase):
  383. cdata = a2b_base64(key_data)
  384. data = self.decrypt_key_file(cdata, passphrase)
  385. if data:
  386. key = msgpack.unpackb(data)
  387. if key[b'version'] != 1:
  388. raise IntegrityError('Invalid key file header')
  389. self.repository_id = key[b'repository_id']
  390. self.enc_key = key[b'enc_key']
  391. self.enc_hmac_key = key[b'enc_hmac_key']
  392. self.id_key = key[b'id_key']
  393. self.chunk_seed = key[b'chunk_seed']
  394. self.tam_required = key.get(b'tam_required', tam_required(self.repository))
  395. return True
  396. return False
  397. def decrypt_key_file(self, data, passphrase):
  398. unpacker = get_limited_unpacker('key')
  399. unpacker.feed(data)
  400. d = unpacker.unpack()
  401. assert d[b'version'] == 1
  402. assert d[b'algorithm'] == b'sha256'
  403. key = passphrase.kdf(d[b'salt'], d[b'iterations'], 32)
  404. data = AES(is_encrypt=False, key=key).decrypt(d[b'data'])
  405. if HMAC(key, data, sha256).digest() == d[b'hash']:
  406. return data
  407. def encrypt_key_file(self, data, passphrase):
  408. salt = os.urandom(32)
  409. iterations = 100000
  410. key = passphrase.kdf(salt, iterations, 32)
  411. hash = HMAC(key, data, sha256).digest()
  412. cdata = AES(is_encrypt=True, key=key).encrypt(data)
  413. d = {
  414. 'version': 1,
  415. 'salt': salt,
  416. 'iterations': iterations,
  417. 'algorithm': 'sha256',
  418. 'hash': hash,
  419. 'data': cdata,
  420. }
  421. return msgpack.packb(d)
  422. def _save(self, passphrase):
  423. key = {
  424. 'version': 1,
  425. 'repository_id': self.repository_id,
  426. 'enc_key': self.enc_key,
  427. 'enc_hmac_key': self.enc_hmac_key,
  428. 'id_key': self.id_key,
  429. 'chunk_seed': self.chunk_seed,
  430. 'tam_required': self.tam_required,
  431. }
  432. data = self.encrypt_key_file(msgpack.packb(key), passphrase)
  433. key_data = '\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii')))
  434. return key_data
  435. def change_passphrase(self, passphrase=None):
  436. if passphrase is None:
  437. passphrase = Passphrase.new(allow_empty=True)
  438. self.save(self.target, passphrase)
  439. @classmethod
  440. def create(cls, repository, args):
  441. passphrase = Passphrase.new(allow_empty=True)
  442. key = cls(repository)
  443. key.repository_id = repository.id
  444. key.init_from_random_data(os.urandom(100))
  445. key.init_ciphers()
  446. target = key.get_new_target(args)
  447. key.save(target, passphrase)
  448. logger.info('Key in "%s" created.' % target)
  449. logger.info('Keep this key safe. Your data will be inaccessible without it.')
  450. return key
  451. def save(self, target, passphrase):
  452. raise NotImplementedError
  453. def get_new_target(self, args):
  454. raise NotImplementedError
  455. class KeyfileKey(KeyfileKeyBase):
  456. TYPE = 0x00
  457. FILE_ID = 'BORG_KEY'
  458. def find_key(self):
  459. file_id = self.FILE_ID.encode()
  460. first_line = file_id + b' ' + hexlify(self.repository.id)
  461. keys_dir = get_keys_dir()
  462. for name in os.listdir(keys_dir):
  463. filename = os.path.join(keys_dir, name)
  464. # we do the magic / id check in binary mode to avoid stumbling over
  465. # decoding errors if somebody has binary files in the keys dir for some reason.
  466. with open(filename, 'rb') as fd:
  467. if fd.read(len(first_line)) == first_line:
  468. return filename
  469. raise KeyfileNotFoundError(self.repository._location.canonical_path(), get_keys_dir())
  470. def get_new_target(self, args):
  471. filename = args.location.to_key_filename()
  472. path = filename
  473. i = 1
  474. while os.path.exists(path):
  475. i += 1
  476. path = filename + '.%d' % i
  477. return path
  478. def load(self, target, passphrase):
  479. with open(target, 'r') as fd:
  480. key_data = ''.join(fd.readlines()[1:])
  481. success = self._load(key_data, passphrase)
  482. if success:
  483. self.target = target
  484. return success
  485. def save(self, target, passphrase):
  486. key_data = self._save(passphrase)
  487. with open(target, 'w') as fd:
  488. fd.write('%s %s\n' % (self.FILE_ID, bin_to_hex(self.repository_id)))
  489. fd.write(key_data)
  490. fd.write('\n')
  491. self.target = target
  492. class RepoKey(KeyfileKeyBase):
  493. TYPE = 0x03
  494. def find_key(self):
  495. loc = self.repository._location.canonical_path()
  496. try:
  497. self.repository.load_key()
  498. return loc
  499. except configparser.NoOptionError:
  500. raise RepoKeyNotFoundError(loc) from None
  501. def get_new_target(self, args):
  502. return self.repository
  503. def load(self, target, passphrase):
  504. # what we get in target is just a repo location, but we already have the repo obj:
  505. target = self.repository
  506. key_data = target.load_key()
  507. key_data = key_data.decode('utf-8') # remote repo: msgpack issue #99, getting bytes
  508. success = self._load(key_data, passphrase)
  509. if success:
  510. self.target = target
  511. return success
  512. def save(self, target, passphrase):
  513. key_data = self._save(passphrase)
  514. key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes
  515. target.save_key(key_data)
  516. self.target = target