Package duplicity :: Module gpg
[hide private]
[frames] | no frames]

Source Code for Module duplicity.gpg

  1  # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- 
  2  # 
  3  # Copyright 2002 Ben Escoto <ben@emerose.org> 
  4  # Copyright 2007 Kenneth Loafman <kenneth@loafman.com> 
  5  # 
  6  # This file is part of duplicity. 
  7  # 
  8  # Duplicity is free software; you can redistribute it and/or modify it 
  9  # under the terms of the GNU General Public License as published by the 
 10  # Free Software Foundation; either version 2 of the License, or (at your 
 11  # option) any later version. 
 12  # 
 13  # Duplicity is distributed in the hope that it will be useful, but 
 14  # WITHOUT ANY WARRANTY; without even the implied warranty of 
 15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 16  # General Public License for more details. 
 17  # 
 18  # You should have received a copy of the GNU General Public License 
 19  # along with duplicity; if not, write to the Free Software Foundation, 
 20  # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 21   
 22  """ 
 23  duplicity's gpg interface, builds upon Frank Tobin's GnuPGInterface 
 24  """ 
 25   
 26  import os, types, tempfile, re, gzip 
 27   
 28  from duplicity import misc 
 29  from duplicity import globals 
 30  from duplicity import GnuPGInterface 
 31   
 32  try: 
 33      from hashlib import sha1 
 34      from hashlib import md5 
 35  except ImportError: 
 36      from sha import new as sha1 
 37      from md5 import new as md5 
 38   
 39  blocksize = 256 * 1024 
 40   
 41   
42 -class GPGError(Exception):
43 """ 44 Indicate some GPG Error 45 """ 46 pass
47 48
49 -class GPGProfile:
50 """ 51 Just hold some GPG settings, avoid passing tons of arguments 52 """
53 - def __init__(self, passphrase = None, sign_key = None, 54 recipients = None):
55 """ 56 Set all data with initializer 57 58 passphrase is the passphrase. If it is None (not ""), assume 59 it hasn't been set. sign_key can be blank if no signing is 60 indicated, and recipients should be a list of keys. For all 61 keys, the format should be an 8 character hex key like 62 'AA0E73D2'. 63 """ 64 assert passphrase is None or type(passphrase) is types.StringType 65 66 self.passphrase = passphrase 67 self.signing_passphrase = passphrase 68 self.sign_key = sign_key 69 self.encrypt_secring = None 70 if recipients is not None: 71 assert type(recipients) is types.ListType # must be list, not tuple 72 self.recipients = recipients 73 else: 74 self.recipients = []
75 76
77 -class GPGFile:
78 """ 79 File-like object that encrypts decrypts another file on the fly 80 """
81 - def __init__(self, encrypt, encrypt_path, profile):
82 """ 83 GPGFile initializer 84 85 If recipients is set, use public key encryption and encrypt to 86 the given keys. Otherwise, use symmetric encryption. 87 88 encrypt_path is the Path of the gpg encrypted file. Right now 89 only symmetric encryption/decryption is supported. 90 91 If passphrase is false, do not set passphrase - GPG program 92 should prompt for it. 93 """ 94 self.status_fp = None # used to find signature 95 self.closed = None # set to true after file closed 96 self.logger_fp = tempfile.TemporaryFile() 97 self.stderr_fp = tempfile.TemporaryFile() 98 self.name = encrypt_path 99 self.byte_count = 0 100 101 # Start GPG process - copied from GnuPGInterface docstring. 102 gnupg = GnuPGInterface.GnuPG() 103 gnupg.options.meta_interactive = 0 104 gnupg.options.extra_args.append('--no-secmem-warning') 105 if globals.use_agent: 106 gnupg.options.extra_args.append('--use-agent') 107 if globals.gpg_options: 108 for opt in globals.gpg_options.split(): 109 gnupg.options.extra_args.append(opt) 110 111 cmdlist = [] 112 if profile.sign_key: 113 gnupg.options.default_key = profile.sign_key 114 cmdlist.append("--sign") 115 # encrypt: sign key needs passphrase 116 # decrypt: encrypt key needs passphrase 117 # special case: allow different symmetric pass with empty sign pass 118 if encrypt and profile.sign_key and profile.signing_passphrase: 119 passphrase = profile.signing_passphrase 120 else: 121 passphrase = profile.passphrase 122 # in case the passphrase is not set, pass an empty one to prevent 123 # TypeError: expected a character buffer object on .write() 124 if passphrase is None: 125 passphrase = "" 126 127 if encrypt: 128 if profile.recipients: 129 gnupg.options.recipients = profile.recipients 130 cmdlist.append('--encrypt') 131 else: 132 cmdlist.append('--symmetric') 133 # use integrity protection 134 gnupg.options.extra_args.append('--force-mdc') 135 # Skip the passphrase if using the agent 136 if globals.use_agent: 137 gnupg_fhs = ['stdin',] 138 else: 139 gnupg_fhs = ['stdin','passphrase'] 140 p1 = gnupg.run(cmdlist, create_fhs=gnupg_fhs, 141 attach_fhs={'stdout': encrypt_path.open("wb"), 142 'stderr': self.stderr_fp, 143 'logger': self.logger_fp}) 144 if not(globals.use_agent): 145 p1.handles['passphrase'].write(passphrase) 146 p1.handles['passphrase'].close() 147 self.gpg_input = p1.handles['stdin'] 148 else: 149 if profile.recipients and profile.encrypt_secring: 150 cmdlist.append('--secret-keyring') 151 cmdlist.append(profile.encrypt_secring) 152 self.status_fp = tempfile.TemporaryFile() 153 # Skip the passphrase if using the agent 154 if globals.use_agent: 155 gnupg_fhs = ['stdout',] 156 else: 157 gnupg_fhs = ['stdout','passphrase'] 158 p1 = gnupg.run(['--decrypt'], create_fhs=gnupg_fhs, 159 attach_fhs={'stdin': encrypt_path.open("rb"), 160 'status': self.status_fp, 161 'stderr': self.stderr_fp, 162 'logger': self.logger_fp}) 163 if not(globals.use_agent): 164 p1.handles['passphrase'].write(passphrase) 165 p1.handles['passphrase'].close() 166 self.gpg_output = p1.handles['stdout'] 167 self.gpg_process = p1 168 self.encrypt = encrypt
169
170 - def read(self, length = -1):
171 try: 172 res = self.gpg_output.read(length) 173 if res is not None: 174 self.byte_count += len(res) 175 except Exception: 176 self.gpg_failed() 177 return res
178
179 - def write(self, buf):
180 try: 181 res = self.gpg_input.write(buf) 182 if res is not None: 183 self.byte_count += len(res) 184 except Exception: 185 self.gpg_failed() 186 return res
187
188 - def tell(self):
189 return self.byte_count
190
191 - def seek(self, offset):
192 assert not self.encrypt 193 assert offset >= self.byte_count, "%d < %d" % (offset, self.byte_count) 194 if offset > self.byte_count: 195 self.read(offset - self.byte_count)
196
197 - def gpg_failed(self):
198 msg = "GPG Failed, see log below:\n" 199 msg += "===== Begin GnuPG log =====\n" 200 for fp in (self.logger_fp, self.stderr_fp): 201 fp.seek(0) 202 for line in fp: 203 msg += line.strip() + "\n" 204 msg += "===== End GnuPG log =====\n" 205 if not (msg.find("invalid packet (ctb=14)") > -1): 206 raise GPGError, msg 207 else: 208 return ""
209
210 - def close(self):
211 if self.encrypt: 212 try: 213 self.gpg_input.close() 214 except Exception: 215 self.gpg_failed() 216 if self.status_fp: 217 self.set_signature() 218 try: 219 self.gpg_process.wait() 220 except Exception: 221 self.gpg_failed() 222 else: 223 res = 1 224 while res: 225 # discard remaining output to avoid GPG error 226 try: 227 res = self.gpg_output.read(blocksize) 228 except Exception: 229 self.gpg_failed() 230 try: 231 self.gpg_output.close() 232 except Exception: 233 self.gpg_failed() 234 if self.status_fp: 235 self.set_signature() 236 try: 237 self.gpg_process.wait() 238 except Exception: 239 self.gpg_failed() 240 self.logger_fp.close() 241 self.stderr_fp.close() 242 self.closed = 1
243
244 - def set_signature(self):
245 """ 246 Set self.signature to 8 character signature keyID 247 248 This only applies to decrypted files. If the file was not 249 signed, set self.signature to None. 250 """ 251 self.status_fp.seek(0) 252 status_buf = self.status_fp.read() 253 match = re.search("^\\[GNUPG:\\] GOODSIG ([0-9A-F]*)", 254 status_buf, re.M) 255 if not match: 256 self.signature = None 257 else: 258 assert len(match.group(1)) >= 8 259 self.signature = match.group(1)[-8:]
260
261 - def get_signature(self):
262 """ 263 Return 8 character keyID of signature, or None if none 264 """ 265 assert self.closed 266 return self.signature
267 268
269 -def GPGWriteFile(block_iter, filename, profile, 270 size = 200 * 1024 * 1024, 271 max_footer_size = 16 * 1024):
272 """ 273 Write GPG compressed file of given size 274 275 This function writes a gpg compressed file by reading from the 276 input iter and writing to filename. When it has read an amount 277 close to the size limit, it "tops off" the incoming data with 278 incompressible data, to try to hit the limit exactly. 279 280 block_iter should have methods .next(size), which returns the next 281 block of data, which should be at most size bytes long. Also 282 .get_footer() returns a string to write at the end of the input 283 file. The footer should have max length max_footer_size. 284 285 Because gpg uses compression, we don't assume that putting 286 bytes_in bytes into gpg will result in bytes_out = bytes_in out. 287 However, do assume that bytes_out <= bytes_in approximately. 288 289 Returns true if succeeded in writing until end of block_iter. 290 """ 291 292 # workaround for circular module imports 293 from duplicity import path 294 295 def top_off(bytes, file): 296 """ 297 Add bytes of incompressible data to to_gpg_fp 298 299 In this case we take the incompressible data from the 300 beginning of filename (it should contain enough because size 301 >> largest block size). 302 """ 303 incompressible_fp = open(filename, "rb") 304 assert misc.copyfileobj(incompressible_fp, file.gpg_input, bytes) == bytes 305 incompressible_fp.close()
306 307 def get_current_size(): 308 return os.stat(filename).st_size 309 310 block_size = 128 * 1024 # don't bother requesting blocks smaller, but also don't ask for bigger 311 target_size = size - 50 * 1024 # fudge factor, compensate for gpg buffering 312 data_size = target_size - max_footer_size 313 file = GPGFile(True, path.Path(filename), profile) 314 at_end_of_blockiter = 0 315 while True: 316 bytes_to_go = data_size - get_current_size() 317 if bytes_to_go < block_size: 318 break 319 try: 320 data = block_iter.next(min(block_size, bytes_to_go)).data 321 except StopIteration: 322 at_end_of_blockiter = 1 323 break 324 file.write(data) 325 326 file.write(block_iter.get_footer()) 327 if not at_end_of_blockiter: 328 # don't pad last volume 329 cursize = get_current_size() 330 if cursize < target_size: 331 top_off(target_size - cursize, file) 332 file.close() 333 return at_end_of_blockiter 334 335
336 -def GzipWriteFile(block_iter, filename, 337 size = 200 * 1024 * 1024, 338 max_footer_size = 16 * 1024):
339 """ 340 Write gzipped compressed file of given size 341 342 This is like the earlier GPGWriteFile except it writes a gzipped 343 file instead of a gpg'd file. This function is somewhat out of 344 place, because it doesn't deal with GPG at all, but it is very 345 similar to GPGWriteFile so they might as well be defined together. 346 347 The input requirements on block_iter and the output is the same as 348 GPGWriteFile (returns true if wrote until end of block_iter). 349 """ 350 class FileCounted: 351 """ 352 Wrapper around file object that counts number of bytes written 353 """ 354 def __init__(self, fileobj): 355 self.fileobj = fileobj 356 self.byte_count = 0
357 def write(self, buf): 358 result = self.fileobj.write(buf) 359 self.byte_count += len(buf) 360 return result 361 def close(self): 362 return self.fileobj.close() 363 364 file_counted = FileCounted(open(filename, "wb")) 365 gzip_file = gzip.GzipFile(None, "wb", 6, file_counted) 366 at_end_of_blockiter = 0 367 while True: 368 bytes_to_go = size - file_counted.byte_count 369 if bytes_to_go < 32 * 1024: 370 break 371 try: 372 new_block = block_iter.next(min(128*1024, bytes_to_go)) 373 except StopIteration: 374 at_end_of_blockiter = 1 375 break 376 gzip_file.write(new_block.data) 377 378 assert not gzip_file.close() and not file_counted.close() 379 return at_end_of_blockiter 380 381
382 -def get_hash(hash, path, hex = 1):
383 """ 384 Return hash of path 385 386 hash should be "MD5" or "SHA1". The output will be in hexadecimal 387 form if hex is true, and in text (base64) otherwise. 388 """ 389 assert path.isreg() 390 fp = path.open("rb") 391 if hash == "SHA1": 392 hash_obj = sha1() 393 elif hash == "MD5": 394 hash_obj = md5() 395 else: 396 assert 0, "Unknown hash %s" % (hash,) 397 398 while 1: 399 buf = fp.read(blocksize) 400 if not buf: 401 break 402 hash_obj.update(buf) 403 assert not fp.close() 404 if hex: 405 return hash_obj.hexdigest() 406 else: 407 return hash_obj.digest()
408