1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
43 """
44 Indicate some GPG Error
45 """
46 pass
47
48
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
72 self.recipients = recipients
73 else:
74 self.recipients = []
75
76
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
95 self.closed = None
96 self.logger_fp = tempfile.TemporaryFile()
97 self.stderr_fp = tempfile.TemporaryFile()
98 self.name = encrypt_path
99 self.byte_count = 0
100
101
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
116
117
118 if encrypt and profile.sign_key and profile.signing_passphrase:
119 passphrase = profile.signing_passphrase
120 else:
121 passphrase = profile.passphrase
122
123
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
134 gnupg.options.extra_args.append('--force-mdc')
135
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
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
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
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
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
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
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
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
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
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
311 target_size = size - 50 * 1024
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
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
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