keymanager.py 7.5 KB

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