1
# Copyright 2002 Ben Escoto
3
# This file is part of duplicity.
5
# duplicity is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation, Inc., 675 Mass Ave, Cambridge MA
8
# 02139, USA; either version 2 of the License, or (at your option) any
9
# later version; incorporated herein by reference.
11
"""duplicity's gpg interface, builds upon Frank Tobin's GnuPGInterface"""
13
import select, os, sys, thread, sha, md5, types, cStringIO, tempfile, re
14
import GnuPGInterface, misc
16
blocksize = 256 * 1024
18
class GPGError(Exception):
19
"""Indicate some GPG Error"""
23
"""Just hold some GPG settings, avoid passing tons of arguments"""
24
def __init__(self, passphrase = None, sign_key = None,
26
"""Set all data with initializer
28
passphrase is the passphrase. If it is None (not ""), assume
29
it hasn't been set. sign_key can be blank if no signing is
30
indicated, and recipients should be a list of keys. For all
31
keys, the format should be an 8 character hex key like
35
assert passphrase is None or type(passphrase) is types.StringType
36
if sign_key: assert recipients # can only sign with asym encryption
38
self.passphrase = passphrase
39
self.sign_key = sign_key
40
if recipients is not None:
41
assert type(recipients) is types.ListType # must be list, not tuple
42
self.recipients = recipients
43
else: self.recipients = []
47
"""File-like object that decrypts another file on the fly"""
48
def __init__(self, encrypt, encrypt_path, profile):
49
"""GPGFile initializer
51
If recipients is set, use public key encryption and encrypt to
52
the given keys. Otherwise, use symmetric encryption.
54
encrypt_path is the Path of the gpg encrypted file. Right now
55
only symmetric encryption/decryption is supported.
57
If passphrase is false, do not set passphrase - GPG program
61
self.status_fp = None # used to find signature
62
self.closed = None # set to true after file closed
64
# Start GPG process - copied from GnuPGInterface docstring.
65
gnupg = GnuPGInterface.GnuPG()
66
gnupg.options.meta_interactive = 0
67
gnupg.options.extra_args.append('--no-secmem-warning')
68
gnupg.passphrase = profile.passphrase
69
if profile.sign_key: gnupg.options.default_key = profile.sign_key
72
if profile.recipients:
73
gnupg.options.recipients = profile.recipients
74
cmdlist = ['--encrypt']
75
if profile.sign_key: cmdlist.append("--sign")
76
else: cmdlist = ['--symmetric']
77
p1 = gnupg.run(cmdlist, create_fhs=['stdin'],
78
attach_fhs={'stdout': encrypt_path.open("wb")})
79
self.gpg_input = p1.handles['stdin']
81
self.status_fp = tempfile.TemporaryFile()
82
p1 = gnupg.run(['--decrypt'], create_fhs=['stdout'],
83
attach_fhs={'stdin': encrypt_path.open("rb"),
84
'status': self.status_fp})
85
self.gpg_output = p1.handles['stdout']
87
self.encrypt = encrypt
89
def read(self, length = -1): return self.gpg_output.read(length)
90
def write(self, buf): return self.gpg_input.write(buf)
94
self.gpg_input.close()
95
if self.status_fp: self.set_signature()
96
self.gpg_process.wait()
98
while self.gpg_output.read(blocksize):
99
pass # discard remaining output to avoid GPG error
100
self.gpg_output.close()
101
if self.status_fp: self.set_signature()
102
self.gpg_process.wait()
105
def set_signature(self):
106
"""Set self.signature to 8 character signature keyID
108
This only applies to decrypted files. If the file was not
109
signed, set self.signature to None.
112
self.status_fp.seek(0)
113
status_buf = self.status_fp.read()
114
match = re.search("^\\[GNUPG:\\] GOODSIG ([0-9A-F]*)",
116
if not match: self.signature = None
118
assert len(match.group(1)) >= 8
119
self.signature = match.group(1)[-8:]
121
def get_signature(self):
122
"""Return 8 character keyID of signature, or None if none"""
124
return self.signature
127
def GPGWriteFile(block_iter, filename, profile,
128
size = 50 * 1024 * 1024, max_footer_size = 16 * 1024):
129
"""Write GPG compressed file of given size
131
This function writes a gpg compressed file by reading from the
132
input iter and writing to filename. When it has read an amount
133
close to the size limit, it "tops off" the incoming data with
134
incompressible data, to try to hit the limit exactly.
136
block_iter should have methods .next(), which returns the next
137
block of data, and .peek(), which returns the next block without
138
deleting it. Also .get_footer() returns a string to write at the
139
end of the input file. The footer should have max length
143
def start_gpg(filename, passphrase):
144
"""Start GPG process, return (process, to_gpg_fileobj)"""
145
gnupg = GnuPGInterface.GnuPG()
146
gnupg.options.meta_interactive = 0
147
gnupg.options.extra_args.append('--no-secmem-warning')
148
gnupg.passphrase = passphrase
149
if profile.sign_key: gnupg.options.default_key = profile.sign_key
151
if profile.recipients:
152
gnupg.options.recipients = profile.recipients
153
cmdlist = ['--encrypt']
154
if profile.sign_key: cmdlist.append("--sign")
155
else: cmdlist = ['--symmetric']
156
p1 = gnupg.run(cmdlist, create_fhs=['stdin'],
157
attach_fhs={'stdout': open(filename, "wb")})
158
return (p1, p1.handles['stdin'])
160
def top_off(bytes, to_gpg_fp):
161
"""Add bytes of incompressible data to to_gpg_fp
163
In this case we take the incompressible data from the
164
beginning of filename (it should contain enough because size
165
>> largest block size).
168
incompressible_fp = open(filename, "rb")
169
assert misc.copyfileobj(incompressible_fp, to_gpg_fp, bytes) == bytes
170
incompressible_fp.close()
172
def get_current_size(): return os.stat(filename).st_size
174
def close_process(gpg_process, to_gpg_fp):
175
"""Close gpg process and clean up"""
179
target_size = size - 18 * 1024 # fudge factor, compensate for gpg buffering
180
check_size = target_size - max_footer_size
181
gpg_process, to_gpg_fp = start_gpg(filename, profile.passphrase)
182
while (block_iter.peek() and
183
get_current_size() + len(block_iter.peek().data) <= check_size):
184
to_gpg_fp.write(block_iter.next().data)
185
to_gpg_fp.write(block_iter.get_footer())
186
if block_iter.peek():
187
cursize = get_current_size()
188
if cursize < target_size: top_off(target_size - cursize, to_gpg_fp)
189
close_process(gpg_process, to_gpg_fp)
192
def get_hash(hash, path, hex = 1):
193
"""Return hash of path
195
hash should be "MD5" or "SHA1". The output will be in hexadecimal
196
form if hex is true, and in text (base64) otherwise.
201
if hash == "SHA1": hash_obj = sha.new()
202
elif hash == "MD5": hash_obj = md5.new()
203
else: assert 0, "Unknown hash %s" % (hash,)
206
buf = fp.read(blocksize)
209
assert not fp.close()
210
if hex: return hash_obj.hexdigest()
211
else: return hash_obj.digest()