~ed.so/duplicity/reuse-passphrase-for-signing-fix

« back to all changes in this revision

Viewing changes to duplicity/gpg.py

  • Committer: bescoto
  • Date: 2002-10-29 01:49:46 UTC
  • Revision ID: vcs-imports@canonical.com-20021029014946-3m4rmm5plom7pl6q
Initial checkin

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2002 Ben Escoto
 
2
#
 
3
# This file is part of duplicity.
 
4
#
 
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.
 
10
 
 
11
"""duplicity's gpg interface, builds upon Frank Tobin's GnuPGInterface"""
 
12
 
 
13
import select, os, sys, thread, sha, md5, types, cStringIO, tempfile, re
 
14
import GnuPGInterface, misc
 
15
 
 
16
blocksize = 256 * 1024
 
17
 
 
18
class GPGError(Exception):
 
19
        """Indicate some GPG Error"""
 
20
        pass
 
21
 
 
22
class GPGProfile:
 
23
        """Just hold some GPG settings, avoid passing tons of arguments"""
 
24
        def __init__(self, passphrase = None, sign_key = None,
 
25
                                 recipients = None):
 
26
                """Set all data with initializer
 
27
 
 
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
 
32
                'AA0E73D2'.
 
33
 
 
34
                """
 
35
                assert passphrase is None or type(passphrase) is types.StringType
 
36
                if sign_key: assert recipients # can only sign with asym encryption
 
37
 
 
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 = []
 
44
 
 
45
 
 
46
class GPGFile:
 
47
        """File-like object that decrypts another file on the fly"""
 
48
        def __init__(self, encrypt, encrypt_path, profile):
 
49
                """GPGFile initializer
 
50
 
 
51
                If recipients is set, use public key encryption and encrypt to
 
52
                the given keys.  Otherwise, use symmetric encryption.
 
53
 
 
54
                encrypt_path is the Path of the gpg encrypted file.  Right now
 
55
                only symmetric encryption/decryption is supported.
 
56
 
 
57
                If passphrase is false, do not set passphrase - GPG program
 
58
                should prompt for it.
 
59
 
 
60
                """
 
61
                self.status_fp = None # used to find signature
 
62
                self.closed = None # set to true after file closed
 
63
 
 
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
 
70
 
 
71
                if encrypt:
 
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']
 
80
                else:
 
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']
 
86
                self.gpg_process = p1
 
87
                self.encrypt = encrypt
 
88
 
 
89
        def read(self, length = -1): return self.gpg_output.read(length)
 
90
        def write(self, buf): return self.gpg_input.write(buf)
 
91
 
 
92
        def close(self):
 
93
                if self.encrypt:
 
94
                        self.gpg_input.close()
 
95
                        if self.status_fp: self.set_signature()
 
96
                        self.gpg_process.wait()
 
97
                else:
 
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()
 
103
                self.closed = 1
 
104
 
 
105
        def set_signature(self):
 
106
                """Set self.signature to 8 character signature keyID
 
107
 
 
108
                This only applies to decrypted files.  If the file was not
 
109
                signed, set self.signature to None.
 
110
 
 
111
                """
 
112
                self.status_fp.seek(0)
 
113
                status_buf = self.status_fp.read()
 
114
                match = re.search("^\\[GNUPG:\\] GOODSIG ([0-9A-F]*)",
 
115
                                                  status_buf, re.M)
 
116
                if not match: self.signature = None
 
117
                else:
 
118
                        assert len(match.group(1)) >= 8
 
119
                        self.signature = match.group(1)[-8:]
 
120
 
 
121
        def get_signature(self):
 
122
                """Return 8 character keyID of signature, or None if none"""
 
123
                assert self.closed
 
124
                return self.signature
 
125
 
 
126
 
 
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
 
130
 
 
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.
 
135
 
 
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
 
140
        max_footer_size.
 
141
 
 
142
        """
 
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
 
150
 
 
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'])
 
159
 
 
160
        def top_off(bytes, to_gpg_fp):
 
161
                """Add bytes of incompressible data to to_gpg_fp
 
162
 
 
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).
 
166
 
 
167
                """
 
168
                incompressible_fp = open(filename, "rb")
 
169
                assert misc.copyfileobj(incompressible_fp, to_gpg_fp, bytes) == bytes
 
170
                incompressible_fp.close()
 
171
 
 
172
        def get_current_size(): return os.stat(filename).st_size
 
173
 
 
174
        def close_process(gpg_process, to_gpg_fp):
 
175
                """Close gpg process and clean up"""
 
176
                to_gpg_fp.close()
 
177
                gpg_process.wait()
 
178
 
 
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)
 
190
 
 
191
 
 
192
def get_hash(hash, path, hex = 1):
 
193
        """Return hash of path
 
194
 
 
195
        hash should be "MD5" or "SHA1".  The output will be in hexadecimal
 
196
        form if hex is true, and in text (base64) otherwise.
 
197
 
 
198
        """
 
199
        assert path.isreg()
 
200
        fp = path.open("rb")
 
201
        if hash == "SHA1": hash_obj = sha.new()
 
202
        elif hash == "MD5": hash_obj = md5.new()
 
203
        else: assert 0, "Unknown hash %s" % (hash,)
 
204
 
 
205
        while 1:
 
206
                buf = fp.read(blocksize)
 
207
                if not buf: break
 
208
                hash_obj.update(buf)
 
209
        assert not fp.close()
 
210
        if hex: return hash_obj.hexdigest()
 
211
        else: return hash_obj.digest()