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