keymanager.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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. # imported here because it has global side effects
  122. import readline
  123. repoid = bin_to_hex(self.repository.id)[:18]
  124. try:
  125. while True: # used for repeating on overall checksum mismatch
  126. # id line input
  127. while True:
  128. idline = input('id: ').replace(' ', '')
  129. if idline == "":
  130. if yes("Abort import? [yN]:"):
  131. raise EOFError()
  132. try:
  133. (data, checksum) = idline.split('-')
  134. except ValueError:
  135. print("each line must contain exactly one '-', try again")
  136. continue
  137. try:
  138. (id_lines, id_repoid, id_complete_checksum) = data.split('/')
  139. except ValueError:
  140. print("the id line must contain exactly three '/', try again")
  141. if sha256_truncated(data.lower().encode('ascii'), 2) != checksum:
  142. print('line checksum did not match, try same line again')
  143. continue
  144. try:
  145. lines = int(id_lines)
  146. except ValueError:
  147. print('internal error while parsing length')
  148. break
  149. if repoid != id_repoid:
  150. raise RepoIdMismatch()
  151. result = b''
  152. idx = 1
  153. # body line input
  154. while True:
  155. inline = input('{0:2d}: '.format(idx))
  156. inline = inline.replace(' ', '')
  157. if inline == "":
  158. if yes("Abort import? [yN]:"):
  159. raise EOFError()
  160. try:
  161. (data, checksum) = inline.split('-')
  162. except ValueError:
  163. print("each line must contain exactly one '-', try again")
  164. continue
  165. try:
  166. part = unhexlify(data)
  167. except binascii.Error:
  168. print("only characters 0-9 and a-f and '-' are valid, try again")
  169. continue
  170. if sha256_truncated(idx.to_bytes(2, byteorder='big') + part, 2) != checksum:
  171. print('line checksum did not match, try line {0} again'.format(idx))
  172. continue
  173. result += part
  174. if idx == lines:
  175. break
  176. idx += 1
  177. if sha256_truncated(result, 12) != id_complete_checksum:
  178. print('The overall checksum did not match, retry or enter a blank line to abort.')
  179. continue
  180. self.keyblob = '\n'.join(textwrap.wrap(b2a_base64(result).decode('ascii'))) + '\n'
  181. self.store_keyblob(args)
  182. break
  183. except EOFError:
  184. print('\n - aborted')
  185. return