1 """Interface to GNU Privacy Guard (GnuPG)
2
3 GnuPGInterface is a Python module to interface with GnuPG.
4 It concentrates on interacting with GnuPG via filehandles,
5 providing access to control GnuPG via versatile and extensible means.
6
7 This module is based on GnuPG::Interface, a Perl module by the same author.
8
9 Normally, using this module will involve creating a
10 GnuPG object, setting some options in it's 'options' data member
11 (which is of type Options), creating some pipes
12 to talk with GnuPG, and then calling the run() method, which will
13 connect those pipes to the GnuPG process. run() returns a
14 Process object, which contains the filehandles to talk to GnuPG with.
15
16 Example code:
17
18 >>> import GnuPGInterface
19 >>>
20 >>> plaintext = "Three blind mice"
21 >>> passphrase = "This is the passphrase"
22 >>>
23 >>> gnupg = GnuPGInterface.GnuPG()
24 >>> gnupg.options.armor = 1
25 >>> gnupg.options.meta_interactive = 0
26 >>> gnupg.options.extra_args.append('--no-secmem-warning')
27 >>>
28 >>> # Normally we might specify something in
29 >>> # gnupg.options.recipients, like
30 >>> # gnupg.options.recipients = [ '0xABCD1234', 'bob@foo.bar' ]
31 >>> # but since we're doing symmetric-only encryption, it's not needed.
32 >>> # If you are doing standard, public-key encryption, using
33 >>> # --encrypt, you will need to specify recipients before
34 >>> # calling gnupg.run()
35 >>>
36 >>> # First we'll encrypt the test_text input symmetrically
37 >>> p1 = gnupg.run(['--symmetric'],
38 ... create_fhs=['stdin', 'stdout', 'passphrase'])
39 >>>
40 >>> p1.handles['passphrase'].write(passphrase)
41 >>> p1.handles['passphrase'].close()
42 >>>
43 >>> p1.handles['stdin'].write(plaintext)
44 >>> p1.handles['stdin'].close()
45 >>>
46 >>> ciphertext = p1.handles['stdout'].read()
47 >>> p1.handles['stdout'].close()
48 >>>
49 >>> # process cleanup
50 >>> p1.wait()
51 >>>
52 >>> # Now we'll decrypt what we just encrypted it,
53 >>> # using the convience method to get the
54 >>> # passphrase to GnuPG
55 >>> gnupg.passphrase = passphrase
56 >>>
57 >>> p2 = gnupg.run(['--decrypt'], create_fhs=['stdin', 'stdout'])
58 >>>
59 >>> p2.handles['stdin'].write(ciphertext)
60 >>> p2.handles['stdin'].close()
61 >>>
62 >>> decrypted_plaintext = p2.handles['stdout'].read()
63 >>> p2.handles['stdout'].close()
64 >>>
65 >>> # process cleanup
66 >>> p2.wait()
67 >>>
68 >>> # Our decrypted plaintext:
69 >>> decrypted_plaintext
70 'Three blind mice'
71 >>>
72 >>> # ...and see it's the same as what we orignally encrypted
73 >>> assert decrypted_plaintext == plaintext, \
74 "GnuPG decrypted output does not match original input"
75 >>>
76 >>>
77 >>> ##################################################
78 >>> # Now let's trying using run()'s attach_fhs paramter
79 >>>
80 >>> # we're assuming we're running on a unix...
81 >>> input = open('/etc/motd')
82 >>>
83 >>> p1 = gnupg.run(['--symmetric'], create_fhs=['stdout'],
84 ... attach_fhs={'stdin': input})
85 >>>
86 >>> # GnuPG will read the stdin from /etc/motd
87 >>> ciphertext = p1.handles['stdout'].read()
88 >>>
89 >>> # process cleanup
90 >>> p1.wait()
91 >>>
92 >>> # Now let's run the output through GnuPG
93 >>> # We'll write the output to a temporary file,
94 >>> import tempfile
95 >>> temp = tempfile.TemporaryFile()
96 >>>
97 >>> p2 = gnupg.run(['--decrypt'], create_fhs=['stdin'],
98 ... attach_fhs={'stdout': temp})
99 >>>
100 >>> # give GnuPG our encrypted stuff from the first run
101 >>> p2.handles['stdin'].write(ciphertext)
102 >>> p2.handles['stdin'].close()
103 >>>
104 >>> # process cleanup
105 >>> p2.wait()
106 >>>
107 >>> # rewind the tempfile and see what GnuPG gave us
108 >>> temp.seek(0)
109 >>> decrypted_plaintext = temp.read()
110 >>>
111 >>> # compare what GnuPG decrypted with our original input
112 >>> input.seek(0)
113 >>> input_data = input.read()
114 >>>
115 >>> assert decrypted_plaintext == input_data, \
116 "GnuPG decrypted output does not match original input"
117
118 To do things like public-key encryption, simply pass do something
119 like:
120
121 gnupg.passphrase = 'My passphrase'
122 gnupg.options.recipients = [ 'bob@foobar.com' ]
123 gnupg.run( ['--sign', '--encrypt'], create_fhs=..., attach_fhs=...)
124
125 Here is an example of subclassing GnuPGInterface.GnuPG,
126 so that it has an encrypt_string() method that returns
127 ciphertext.
128
129 >>> import GnuPGInterface
130 >>>
131 >>> class MyGnuPG(GnuPGInterface.GnuPG):
132 ...
133 ... def __init__(self):
134 ... GnuPGInterface.GnuPG.__init__(self)
135 ... self.setup_my_options()
136 ...
137 ... def setup_my_options(self):
138 ... self.options.armor = 1
139 ... self.options.meta_interactive = 0
140 ... self.options.extra_args.append('--no-secmem-warning')
141 ...
142 ... def encrypt_string(self, string, recipients):
143 ... gnupg.options.recipients = recipients # a list!
144 ...
145 ... proc = gnupg.run(['--encrypt'], create_fhs=['stdin', 'stdout'])
146 ...
147 ... proc.handles['stdin'].write(string)
148 ... proc.handles['stdin'].close()
149 ...
150 ... output = proc.handles['stdout'].read()
151 ... proc.handles['stdout'].close()
152 ...
153 ... proc.wait()
154 ... return output
155 ...
156 >>> gnupg = MyGnuPG()
157 >>> ciphertext = gnupg.encrypt_string("The secret", ['0x260C4FA3'])
158 >>>
159 >>> # just a small sanity test here for doctest
160 >>> import types
161 >>> assert isinstance(ciphertext, types.StringType), \
162 "What GnuPG gave back is not a string!"
163
164 Here is an example of generating a key:
165 >>> import GnuPGInterface
166 >>> gnupg = GnuPGInterface.GnuPG()
167 >>> gnupg.options.meta_interactive = 0
168 >>>
169 >>> # We will be creative and use the logger filehandle to capture
170 >>> # what GnuPG says this time, instead stderr; no stdout to listen to,
171 >>> # but we capture logger to surpress the dry-run command.
172 >>> # We also have to capture stdout since otherwise doctest complains;
173 >>> # Normally you can let stdout through when generating a key.
174 >>>
175 >>> proc = gnupg.run(['--gen-key'], create_fhs=['stdin', 'stdout',
176 ... 'logger'])
177 >>>
178 >>> proc.handles['stdin'].write('''Key-Type: DSA
179 ... Key-Length: 1024
180 ... # We are only testing syntax this time, so dry-run
181 ... %dry-run
182 ... Subkey-Type: ELG-E
183 ... Subkey-Length: 1024
184 ... Name-Real: Joe Tester
185 ... Name-Comment: with stupid passphrase
186 ... Name-Email: joe@foo.bar
187 ... Expire-Date: 2y
188 ... Passphrase: abc
189 ... %pubring foo.pub
190 ... %secring foo.sec
191 ... ''')
192 >>>
193 >>> proc.handles['stdin'].close()
194 >>>
195 >>> report = proc.handles['logger'].read()
196 >>> proc.handles['logger'].close()
197 >>>
198 >>> proc.wait()
199
200
201 COPYRIGHT:
202
203 Copyright (C) 2001 Frank J. Tobin, ftobin@neverending.org
204
205 LICENSE:
206
207 This library is free software; you can redistribute it and/or
208 modify it under the terms of the GNU Lesser General Public
209 License as published by the Free Software Foundation; either
210 version 2.1 of the License, or (at your option) any later version.
211
212 This library is distributed in the hope that it will be useful,
213 but WITHOUT ANY WARRANTY; without even the implied warranty of
214 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
215 Lesser General Public License for more details.
216
217 You should have received a copy of the GNU Lesser General Public
218 License along with this library; if not, write to the Free Software
219 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
220 or see http://www.gnu.org/copyleft/lesser.html
221 """
222
223 import os
224 import sys
225 import fcntl
226
227 from duplicity import log
228
229 try:
230 import threading
231 except ImportError:
232 import dummy_threading
233 log.Warn("Threading not available -- zombie processes may appear")
234
235 __author__ = "Frank J. Tobin, ftobin@neverending.org"
236 __version__ = "0.3.2"
237 __revision__ = "$Id: duplicity.GnuPGInterface-pysrc.html,v 1.14 2011/11/25 19:41:03 loafman Exp $"
238
239
240 _stds = [ 'stdin', 'stdout', 'stderr' ]
241
242
243 _fd_modes = { 'stdin': 'w',
244 'stdout': 'r',
245 'stderr': 'r',
246 'passphrase': 'w',
247 'command': 'w',
248 'logger': 'r',
249 'status': 'r'
250 }
251
252
253 _fd_options = { 'passphrase': '--passphrase-fd',
254 'logger': '--logger-fd',
255 'status': '--status-fd',
256 'command': '--command-fd' }
257
259 """Class instances represent GnuPG.
260
261 Instance attributes of a GnuPG object are:
262
263 * call -- string to call GnuPG with. Defaults to "gpg"
264
265 * passphrase -- Since it is a common operation
266 to pass in a passphrase to GnuPG,
267 and working with the passphrase filehandle mechanism directly
268 can be mundane, if set, the passphrase attribute
269 works in a special manner. If the passphrase attribute is set,
270 and no passphrase file object is sent in to run(),
271 then GnuPG instnace will take care of sending the passphrase to
272 GnuPG, the executable instead of having the user sent it in manually.
273
274 * options -- Object of type GnuPGInterface.Options.
275 Attribute-setting in options determines
276 the command-line options used when calling GnuPG.
277 """
278
280 self.call = 'gpg'
281 self.passphrase = None
282 self.options = Options()
283
284 - def run(self, gnupg_commands, args=None, create_fhs=None, attach_fhs=None):
285 """Calls GnuPG with the list of string commands gnupg_commands,
286 complete with prefixing dashes.
287 For example, gnupg_commands could be
288 '["--sign", "--encrypt"]'
289 Returns a GnuPGInterface.Process object.
290
291 args is an optional list of GnuPG command arguments (not options),
292 such as keyID's to export, filenames to process, etc.
293
294 create_fhs is an optional list of GnuPG filehandle
295 names that will be set as keys of the returned Process object's
296 'handles' attribute. The generated filehandles can be used
297 to communicate with GnuPG via standard input, standard output,
298 the status-fd, passphrase-fd, etc.
299
300 Valid GnuPG filehandle names are:
301 * stdin
302 * stdout
303 * stderr
304 * status
305 * passphase
306 * command
307 * logger
308
309 The purpose of each filehandle is described in the GnuPG
310 documentation.
311
312 attach_fhs is an optional dictionary with GnuPG filehandle
313 names mapping to opened files. GnuPG will read or write
314 to the file accordingly. For example, if 'my_file' is an
315 opened file and 'attach_fhs[stdin] is my_file', then GnuPG
316 will read its standard input from my_file. This is useful
317 if you want GnuPG to read/write to/from an existing file.
318 For instance:
319
320 f = open("encrypted.gpg")
321 gnupg.run(["--decrypt"], attach_fhs={'stdin': f})
322
323 Using attach_fhs also helps avoid system buffering
324 issues that can arise when using create_fhs, which
325 can cause the process to deadlock.
326
327 If not mentioned in create_fhs or attach_fhs,
328 GnuPG filehandles which are a std* (stdin, stdout, stderr)
329 are defaulted to the running process' version of handle.
330 Otherwise, that type of handle is simply not used when calling GnuPG.
331 For example, if you do not care about getting data from GnuPG's
332 status filehandle, simply do not specify it.
333
334 run() returns a Process() object which has a 'handles'
335 which is a dictionary mapping from the handle name
336 (such as 'stdin' or 'stdout') to the respective
337 newly-created FileObject connected to the running GnuPG process.
338 For instance, if the call was
339
340 process = gnupg.run(["--decrypt"], stdin=1)
341
342 after run returns 'process.handles["stdin"]'
343 is a FileObject connected to GnuPG's standard input,
344 and can be written to.
345 """
346
347 if args == None: args = []
348 if create_fhs == None: create_fhs = []
349 if attach_fhs == None: attach_fhs = {}
350
351 for std in _stds:
352 if not attach_fhs.has_key(std) \
353 and std not in create_fhs:
354 attach_fhs.setdefault(std, getattr(sys, std))
355
356 handle_passphrase = 0
357
358 if self.passphrase != None \
359 and not attach_fhs.has_key('passphrase') \
360 and 'passphrase' not in create_fhs:
361 handle_passphrase = 1
362 create_fhs.append('passphrase')
363
364 process = self._attach_fork_exec(gnupg_commands, args,
365 create_fhs, attach_fhs)
366
367 if handle_passphrase:
368 passphrase_fh = process.handles['passphrase']
369 passphrase_fh.write( self.passphrase )
370 passphrase_fh.close()
371 del process.handles['passphrase']
372
373 return process
374
375
377 """This is like run(), but without the passphrase-helping
378 (note that run() calls this)."""
379
380 process = Process()
381
382 for fh_name in create_fhs + attach_fhs.keys():
383 if not _fd_modes.has_key(fh_name):
384 raise KeyError, \
385 "unrecognized filehandle name '%s'; must be one of %s" \
386 % (fh_name, _fd_modes.keys())
387
388 for fh_name in create_fhs:
389
390
391 if attach_fhs.has_key(fh_name):
392 raise ValueError, \
393 "cannot have filehandle '%s' in both create_fhs and attach_fhs" \
394 % fh_name
395
396 pipe = os.pipe()
397
398
399
400
401 if _fd_modes[fh_name] == 'w': pipe = (pipe[1], pipe[0])
402 process._pipes[fh_name] = Pipe(pipe[0], pipe[1], 0)
403
404 for fh_name, fh in attach_fhs.items():
405 process._pipes[fh_name] = Pipe(fh.fileno(), fh.fileno(), 1)
406
407 process.pid = os.fork()
408 if process.pid != 0:
409
410 process.thread = threading.Thread(target=threaded_waitpid,
411 name="wait%d" % process.pid,
412 args=(process,))
413 process.thread.start()
414
415 if process.pid == 0: self._as_child(process, gnupg_commands, args)
416 return self._as_parent(process)
417
418
420 """Stuff run after forking in parent"""
421 for k, p in process._pipes.items():
422 if not p.direct:
423 os.close(p.child)
424 process.handles[k] = os.fdopen(p.parent, _fd_modes[k])
425
426
427 del process._pipes
428
429 return process
430
431
432 - def _as_child(self, process, gnupg_commands, args):
433 """Stuff run after forking in child"""
434
435 for std in _stds:
436 p = process._pipes[std]
437 os.dup2( p.child, getattr(sys, "__%s__" % std).fileno() )
438
439 for k, p in process._pipes.items():
440 if p.direct and k not in _stds:
441
442 fcntl.fcntl( p.child, fcntl.F_SETFD, 0 )
443
444 fd_args = []
445
446 for k, p in process._pipes.items():
447
448 if k not in _stds:
449 fd_args.extend([ _fd_options[k], "%d" % p.child ])
450
451 if not p.direct: os.close(p.parent)
452
453 command = [ self.call ] + fd_args + self.options.get_args() \
454 + gnupg_commands + args
455
456 os.execvp( command[0], command )
457
458
460 """simple struct holding stuff about pipes we use"""
461 - def __init__(self, parent, child, direct):
462 self.parent = parent
463 self.child = child
464 self.direct = direct
465
466
468 """Objects of this class encompass options passed to GnuPG.
469 This class is responsible for determining command-line arguments
470 which are based on options. It can be said that a GnuPG
471 object has-a Options object in its options attribute.
472
473 Attributes which correlate directly to GnuPG options:
474
475 Each option here defaults to false or None, and is described in
476 GnuPG documentation.
477
478 Booleans (set these attributes to booleans)
479
480 * armor
481 * no_greeting
482 * no_verbose
483 * quiet
484 * batch
485 * always_trust
486 * rfc1991
487 * openpgp
488 * force_v3_sigs
489 * no_options
490 * textmode
491
492 Strings (set these attributes to strings)
493
494 * homedir
495 * default_key
496 * comment
497 * compress_algo
498 * options
499
500 Lists (set these attributes to lists)
501
502 * recipients (***NOTE*** plural of 'recipient')
503 * encrypt_to
504
505 Meta options
506
507 Meta options are options provided by this module that do
508 not correlate directly to any GnuPG option by name,
509 but are rather bundle of options used to accomplish
510 a specific goal, such as obtaining compatibility with PGP 5.
511 The actual arguments each of these reflects may change with time. Each
512 defaults to false unless otherwise specified.
513
514 meta_pgp_5_compatible -- If true, arguments are generated to try
515 to be compatible with PGP 5.x.
516
517 meta_pgp_2_compatible -- If true, arguments are generated to try
518 to be compatible with PGP 2.x.
519
520 meta_interactive -- If false, arguments are generated to try to
521 help the using program use GnuPG in a non-interactive
522 environment, such as CGI scripts. Default is true.
523
524 extra_args -- Extra option arguments may be passed in
525 via the attribute extra_args, a list.
526
527 >>> import GnuPGInterface
528 >>>
529 >>> gnupg = GnuPGInterface.GnuPG()
530 >>> gnupg.options.armor = 1
531 >>> gnupg.options.recipients = ['Alice', 'Bob']
532 >>> gnupg.options.extra_args = ['--no-secmem-warning']
533 >>>
534 >>> # no need for users to call this normally; just for show here
535 >>> gnupg.options.get_args()
536 ['--armor', '--recipient', 'Alice', '--recipient', 'Bob', '--no-secmem-warning']
537 """
538
540
541 self.armor = 0
542 self.no_greeting = 0
543 self.verbose = 0
544 self.no_verbose = 0
545 self.quiet = 0
546 self.batch = 0
547 self.always_trust = 0
548 self.rfc1991 = 0
549 self.openpgp = 0
550 self.force_v3_sigs = 0
551 self.no_options = 0
552 self.textmode = 0
553
554
555 self.meta_pgp_5_compatible = 0
556 self.meta_pgp_2_compatible = 0
557 self.meta_interactive = 1
558
559
560 self.homedir = None
561 self.default_key = None
562 self.comment = None
563 self.compress_algo = None
564 self.options = None
565
566
567 self.encrypt_to = []
568 self.recipients = []
569
570
571 self.extra_args = []
572
577
579 """Generate a list of standard, non-meta or extra arguments"""
580 args = []
581 if self.homedir != None: args.extend( [ '--homedir', self.homedir ] )
582 if self.options != None: args.extend( [ '--options', self.options ] )
583 if self.comment != None: args.extend( [ '--comment', self.comment ] )
584 if self.compress_algo != None: args.extend( [ '--compress-algo', self.compress_algo ] )
585 if self.default_key != None: args.extend( [ '--default-key', self.default_key ] )
586
587 if self.no_options: args.append( '--no-options' )
588 if self.armor: args.append( '--armor' )
589 if self.textmode: args.append( '--textmode' )
590 if self.no_greeting: args.append( '--no-greeting' )
591 if self.verbose: args.append( '--verbose' )
592 if self.no_verbose: args.append( '--no-verbose' )
593 if self.quiet: args.append( '--quiet' )
594 if self.batch: args.append( '--batch' )
595 if self.always_trust: args.append( '--always-trust' )
596 if self.force_v3_sigs: args.append( '--force-v3-sigs' )
597 if self.rfc1991: args.append( '--rfc1991' )
598 if self.openpgp: args.append( '--openpgp' )
599
600 for r in self.recipients: args.extend( [ '--recipient', r ] )
601 for r in self.encrypt_to: args.extend( [ '--encrypt-to', r ] )
602
603 return args
604
616
617
619 """Objects of this class encompass properties of a GnuPG
620 process spawned by GnuPG.run().
621
622 # gnupg is a GnuPG object
623 process = gnupg.run( [ '--decrypt' ], stdout = 1 )
624 out = process.handles['stdout'].read()
625 ...
626 os.waitpid( process.pid, 0 )
627
628 Data Attributes
629
630 handles -- This is a map of filehandle-names to
631 the file handles, if any, that were requested via run() and hence
632 are connected to the running GnuPG process. Valid names
633 of this map are only those handles that were requested.
634
635 pid -- The PID of the spawned GnuPG process.
636 Useful to know, since once should call
637 os.waitpid() to clean up the process, especially
638 if multiple calls are made to run().
639 """
640
642 self._pipes = {}
643 self.handles = {}
644 self.pid = None
645 self._waited = None
646 self.thread = None
647 self.returned = None
648
650 """
651 Wait on threaded_waitpid to exit and examine results.
652 Will raise an IOError if the process exits non-zero.
653 """
654 if self.returned == None:
655 self.thread.join()
656 if self.returned != 0:
657 raise IOError, "GnuPG exited non-zero, with code %d" % (self.returned >> 8)
658
659
661 """
662 When started as a thread with the Process object, thread
663 will execute an immediate waitpid() against the process
664 pid and will collect the process termination info. This
665 will allow us to reap child processes as soon as possible,
666 thus freeing resources quickly.
667 """
668 try:
669 process.returned = os.waitpid(process.pid, 0)[1]
670 except:
671 log.Debug("GPG process %d terminated before wait()" % process.pid)
672 process.returned = 0
673
674
678
679
680 GnuPGInterface = GnuPG
681
682 if __name__ == '__main__':
683 _run_doctests()
684