keymanager.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. from binascii import unhexlify, a2b_base64, b2a_base64
  2. import binascii
  3. import textwrap
  4. from hashlib import sha256
  5. import pkgutil
  6. from .key import KeyfileKey, RepoKey, PassphraseKey, KeyfileNotFoundError, PlaintextKey
  7. from .helpers import Manifest, NoManifestError, Error, yes, bin_to_hex
  8. from .repository import Repository
  9. class UnencryptedRepo(Error):
  10. """Keymanagement not available for unencrypted repositories."""
  11. class UnknownKeyType(Error):
  12. """Keytype {0} is unknown."""
  13. class RepoIdMismatch(Error):
  14. """This key backup seems to be for a different backup repository, aborting."""
  15. class NotABorgKeyFile(Error):
  16. """This file is not a borg key backup, aborting."""
  17. def sha256_truncated(data, num):
  18. h = sha256()
  19. h.update(data)
  20. return h.hexdigest()[:num]
  21. KEYBLOB_LOCAL = 'local'
  22. KEYBLOB_REPO = 'repo'
  23. class KeyManager:
  24. def __init__(self, repository):
  25. self.repository = repository
  26. self.keyblob = None
  27. self.keyblob_storage = None
  28. try:
  29. cdata = self.repository.get(Manifest.MANIFEST_ID)
  30. except Repository.ObjectNotFound:
  31. raise NoManifestError
  32. key_type = cdata[0]
  33. if key_type == KeyfileKey.TYPE:
  34. self.keyblob_storage = KEYBLOB_LOCAL
  35. elif key_type == RepoKey.TYPE or key_type == PassphraseKey.TYPE:
  36. self.keyblob_storage = KEYBLOB_REPO
  37. elif key_type == PlaintextKey.TYPE:
  38. raise UnencryptedRepo()
  39. else:
  40. raise UnknownKeyType(key_type)
  41. def load_keyblob(self):
  42. if self.keyblob_storage == KEYBLOB_LOCAL:
  43. k = KeyfileKey(self.repository)
  44. target = k.find_key()
  45. with open(target, 'r') as fd:
  46. self.keyblob = ''.join(fd.readlines()[1:])
  47. elif self.keyblob_storage == KEYBLOB_REPO:
  48. self.keyblob = self.repository.load_key().decode()
  49. def store_keyblob(self, args):
  50. if self.keyblob_storage == KEYBLOB_LOCAL:
  51. k = KeyfileKey(self.repository)
  52. try:
  53. target = k.find_key()
  54. except KeyfileNotFoundError:
  55. target = k.get_new_target(args)
  56. self.store_keyfile(target)
  57. elif self.keyblob_storage == KEYBLOB_REPO:
  58. self.repository.save_key(self.keyblob.encode('utf-8'))
  59. def get_keyfile_data(self):
  60. data = '%s %s\n' % (KeyfileKey.FILE_ID, bin_to_hex(self.repository.id))
  61. data += self.keyblob
  62. if not self.keyblob.endswith('\n'):
  63. data += '\n'
  64. return data
  65. def store_keyfile(self, target):
  66. with open(target, 'w') as fd:
  67. fd.write(self.get_keyfile_data())
  68. def export(self, path):
  69. self.store_keyfile(path)
  70. def export_qr(self, path):
  71. with open(path, 'wb') as fd:
  72. key_data = self.get_keyfile_data()
  73. html = pkgutil.get_data('borg', 'paperkey.html')
  74. html = html.replace(b'</textarea>', key_data.encode() + b'</textarea>')
  75. fd.write(html)
  76. def export_paperkey(self, path):
  77. def grouped(s):
  78. ret = ''
  79. i = 0
  80. for ch in s:
  81. if i and i % 6 == 0:
  82. ret += ' '
  83. ret += ch
  84. i += 1
  85. return ret
  86. export = 'To restore key use borg key import --paper /path/to/repo\n\n'
  87. binary = a2b_base64(self.keyblob)
  88. export += 'BORG PAPER KEY v1\n'
  89. lines = (len(binary) + 17) // 18
  90. repoid = bin_to_hex(self.repository.id)[:18]
  91. complete_checksum = sha256_truncated(binary, 12)
  92. export += 'id: {0:d} / {1} / {2} - {3}\n'.format(lines,
  93. grouped(repoid),
  94. grouped(complete_checksum),
  95. sha256_truncated((str(lines) + '/' + repoid + '/' + complete_checksum).encode('ascii'), 2))
  96. idx = 0
  97. while len(binary):
  98. idx += 1
  99. binline = binary[:18]
  100. checksum = sha256_truncated(idx.to_bytes(2, byteorder='big') + binline, 2)
  101. export += '{0:2d}: {1} - {2}\n'.format(idx, grouped(bin_to_hex(binline)), checksum)
  102. binary = binary[18:]
  103. if path:
  104. with open(path, 'w') as fd:
  105. fd.write(export)
  106. else:
  107. print(export)
  108. def import_keyfile(self, args):
  109. file_id = KeyfileKey.FILE_ID
  110. first_line = file_id + ' ' + bin_to_hex(self.repository.id) + '\n'
  111. with open(args.path, 'r') as fd:
  112. file_first_line = fd.read(len(first_line))
  113. if file_first_line != first_line:
  114. if not file_first_line.startswith(file_id):
  115. raise NotABorgKeyFile()
  116. else:
  117. raise RepoIdMismatch()
  118. self.keyblob = fd.read()
  119. self.store_keyblob(args)
  120. def import_paperkey(self, args):
  121. try:
  122. # imported here because it has global side effects
  123. import readline
  124. except ImportError:
  125. print('Note: No line editing available due to missing readline support')
  126. repoid = bin_to_hex(self.repository.id)[:18]
  127. try:
  128. while True: # used for repeating on overall checksum mismatch
  129. # id line input
  130. while True:
  131. idline = input('id: ').replace(' ', '')
  132. if idline == '':
  133. if yes('Abort import? [yN]:'):
  134. raise EOFError()
  135. try:
  136. (data, checksum) = idline.split('-')
  137. except ValueError:
  138. print("each line must contain exactly one '-', try again")
  139. continue
  140. try:
  141. (id_lines, id_repoid, id_complete_checksum) = data.split('/')
  142. except ValueError:
  143. print("the id line must contain exactly three '/', try again")
  144. continue
  145. if sha256_truncated(data.lower().encode('ascii'), 2) != checksum:
  146. print('line checksum did not match, try same line again')
  147. continue
  148. try:
  149. lines = int(id_lines)
  150. except ValueError:
  151. print('internal error while parsing length')
  152. break
  153. if repoid != id_repoid:
  154. raise RepoIdMismatch()
  155. result = b''
  156. idx = 1
  157. # body line input
  158. while True:
  159. inline = input('{0:2d}: '.format(idx))
  160. inline = inline.replace(' ', '')
  161. if inline == '':
  162. if yes('Abort import? [yN]:'):
  163. raise EOFError()
  164. try:
  165. (data, checksum) = inline.split('-')
  166. except ValueError:
  167. print("each line must contain exactly one '-', try again")
  168. continue
  169. try:
  170. part = unhexlify(data)
  171. except binascii.Error:
  172. print("only characters 0-9 and a-f and '-' are valid, try again")
  173. continue
  174. if sha256_truncated(idx.to_bytes(2, byteorder='big') + part, 2) != checksum:
  175. print('line checksum did not match, try line {0} again'.format(idx))
  176. continue
  177. result += part
  178. if idx == lines:
  179. break
  180. idx += 1
  181. if sha256_truncated(result, 12) != id_complete_checksum:
  182. print('The overall checksum did not match, retry or enter a blank line to abort.')
  183. continue
  184. self.keyblob = '\n'.join(textwrap.wrap(b2a_base64(result).decode('ascii'))) + '\n'
  185. self.store_keyblob(args)
  186. break
  187. except EOFError:
  188. print('\n - aborted')
  189. return