root/tags/gajim-0.11.4/src/common/GnuPGInterface.py

Revision 6219, 21.7 kB (checked in by asterix, 3 years ago)

GnuPGInterface can't work under windows.

  • Property svn:eol-style set to LF
Line 
1"""Interface to GNU Privacy Guard (GnuPG)
2
3GnuPGInterface is a Python module to interface with GnuPG.
4It concentrates on interacting with GnuPG via filehandles,
5providing access to control GnuPG via versatile and extensible means.
6
7This module is based on GnuPG::Interface, a Perl module by the same author.
8
9Normally, using this module will involve creating a
10GnuPG object, setting some options in it's 'options' data member
11(which is of type Options), creating some pipes
12to talk with GnuPG, and then calling the run() method, which will
13connect those pipes to the GnuPG process. run() returns a
14Process object, which contains the filehandles to talk to GnuPG with.
15
16Example 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
118To do things like public-key encryption, simply pass do something
119like:
120
121gnupg.passphrase = 'My passphrase'
122gnupg.options.recipients = [ 'bob@foobar.com' ]
123gnupg.run( ['--sign', '--encrypt'], create_fhs=..., attach_fhs=...)
124
125Here is an example of subclassing GnuPGInterface.GnuPG,
126so that it has an encrypt_string() method that returns
127ciphertext.
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
164Here 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
201COPYRIGHT:
202
203Copyright (C) 2001  Frank J. Tobin, ftobin@neverending.org
204
205LICENSE:
206
207This library is free software; you can redistribute it and/or
208modify it under the terms of the GNU Lesser General Public
209License as published by the Free Software Foundation; either
210version 2.1 of the License, or (at your option) any later version.
211
212This library is distributed in the hope that it will be useful,
213but WITHOUT ANY WARRANTY; without even the implied warranty of
214MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
215Lesser General Public License for more details.
216
217You should have received a copy of the GNU Lesser General Public
218License along with this library; if not, write to the Free Software
219Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
220or see http://www.gnu.org/copyleft/lesser.html
221"""
222
223import os
224import sys
225import fcntl
226
227__author__   = "Frank J. Tobin, ftobin@neverending.org"
228__version__  = "0.3.2"
229__revision__ = "$Id: GnuPGInterface.py,v 1.22 2002/01/11 20:22:04 ftobin Exp $"
230
231# "standard" filehandles attached to processes
232_stds = [ 'stdin', 'stdout', 'stderr' ]
233
234# the permissions each type of fh needs to be opened with
235_fd_modes = { 'stdin':      'w',
236              'stdout':     'r',
237              'stderr':     'r',
238              'passphrase': 'w',
239              'command':    'w',
240              'logger':     'r',
241              'status':     'r'
242              }
243
244# correlation between handle names and the arguments we'll pass
245_fd_options = { 'passphrase': '--passphrase-fd',
246                'logger':     '--logger-fd',
247                'status':     '--status-fd',
248                'command':    '--command-fd' }
249
250class GnuPG:
251    """Class instances represent GnuPG.
252   
253    Instance attributes of a GnuPG object are:
254   
255    * call -- string to call GnuPG with.  Defaults to "gpg"
256
257    * passphrase -- Since it is a common operation
258      to pass in a passphrase to GnuPG,
259      and working with the passphrase filehandle mechanism directly
260      can be mundane, if set, the passphrase attribute
261      works in a special manner.  If the passphrase attribute is set,
262      and no passphrase file object is sent in to run(),
263      then GnuPG instnace will take care of sending the passphrase to
264      GnuPG, the executable instead of having the user sent it in manually.
265     
266    * options -- Object of type GnuPGInterface.Options.
267      Attribute-setting in options determines
268      the command-line options used when calling GnuPG.
269    """
270
271    def __init__(self):
272        self.call = 'gpg'
273        self.passphrase = None
274        self.options = Options()
275   
276    def run(self, gnupg_commands, args=None, create_fhs=None, attach_fhs=None):
277        """Calls GnuPG with the list of string commands gnupg_commands,
278        complete with prefixing dashes.
279        For example, gnupg_commands could be
280        '["--sign", "--encrypt"]'
281        Returns a GnuPGInterface.Process object.
282       
283        args is an optional list of GnuPG command arguments (not options),
284        such as keyID's to export, filenames to process, etc.
285
286        create_fhs is an optional list of GnuPG filehandle
287        names that will be set as keys of the returned Process object's
288        'handles' attribute.  The generated filehandles can be used
289        to communicate with GnuPG via standard input, standard output,
290        the status-fd, passphrase-fd, etc.
291       
292        Valid GnuPG filehandle names are:
293          * stdin
294          * stdout
295          * stderr
296          * status
297          * passphase
298          * command
299          * logger
300       
301        The purpose of each filehandle is described in the GnuPG
302        documentation.
303       
304        attach_fhs is an optional dictionary with GnuPG filehandle
305        names mapping to opened files.  GnuPG will read or write
306        to the file accordingly.  For example, if 'my_file' is an
307        opened file and 'attach_fhs[stdin] is my_file', then GnuPG
308        will read its standard input from my_file. This is useful
309        if you want GnuPG to read/write to/from an existing file.
310        For instance:
311       
312            f = open("encrypted.gpg")
313            gnupg.run(["--decrypt"], attach_fhs={'stdin': f})
314
315        Using attach_fhs also helps avoid system buffering
316        issues that can arise when using create_fhs, which
317        can cause the process to deadlock.
318       
319        If not mentioned in create_fhs or attach_fhs,
320        GnuPG filehandles which are a std* (stdin, stdout, stderr)
321        are defaulted to the running process' version of handle.
322        Otherwise, that type of handle is simply not used when calling GnuPG.
323        For example, if you do not care about getting data from GnuPG's
324        status filehandle, simply do not specify it.
325       
326        run() returns a Process() object which has a 'handles'
327        which is a dictionary mapping from the handle name
328        (such as 'stdin' or 'stdout') to the respective
329        newly-created FileObject connected to the running GnuPG process.
330        For instance, if the call was
331
332          process = gnupg.run(["--decrypt"], stdin=1)
333         
334        after run returns 'process.handles["stdin"]'
335        is a FileObject connected to GnuPG's standard input,
336        and can be written to.
337        """
338       
339        if args == None: args = []
340        if create_fhs == None: create_fhs = []
341        if attach_fhs == None: attach_fhs = {}
342       
343        for std in _stds:
344            if not attach_fhs.has_key(std) \
345               and std not in create_fhs:
346                attach_fhs.setdefault(std, getattr(sys, std))
347       
348        handle_passphrase = 0
349       
350        if self.passphrase != None \
351           and not attach_fhs.has_key('passphrase') \
352           and 'passphrase' not in create_fhs:
353            handle_passphrase = 1
354            create_fhs.append('passphrase')
355       
356        process = self._attach_fork_exec(gnupg_commands, args,
357                                         create_fhs, attach_fhs)
358       
359        if handle_passphrase:
360            passphrase_fh = process.handles['passphrase']
361            passphrase_fh.write( self.passphrase )
362            passphrase_fh.close()
363            del process.handles['passphrase']
364       
365        return process
366   
367   
368    def _attach_fork_exec(self, gnupg_commands, args, create_fhs, attach_fhs):
369        """This is like run(), but without the passphrase-helping
370        (note that run() calls this)."""
371       
372        process = Process()
373       
374        for fh_name in create_fhs + attach_fhs.keys():
375            if not _fd_modes.has_key(fh_name):
376                raise KeyError, \
377                      "unrecognized filehandle name '%s'; must be one of %s" \
378                      % (fh_name, _fd_modes.keys())
379
380        for fh_name in create_fhs:
381            # make sure the user doesn't specify a filehandle
382            # to be created *and* attached
383            if attach_fhs.has_key(fh_name):
384                raise ValueError, \
385                      "cannot have filehandle '%s' in both create_fhs and attach_fhs" \
386                      % fh_name
387
388            pipe = os.pipe()
389            # fix by drt@un.bewaff.net noting
390            # that since pipes are unidirectional on some systems,
391            # so we have to 'turn the pipe around'
392            # if we are writing
393            if _fd_modes[fh_name] == 'w': pipe = (pipe[1], pipe[0])
394            process._pipes[fh_name] = Pipe(pipe[0], pipe[1], 0)
395       
396        for fh_name, fh in attach_fhs.items():
397            process._pipes[fh_name] = Pipe(fh.fileno(), fh.fileno(), 1)
398       
399        process.pid = os.fork()
400       
401        if process.pid == 0: self._as_child(process, gnupg_commands, args)
402        return self._as_parent(process)
403   
404   
405    def _as_parent(self, process):
406        """Stuff run after forking in parent"""
407        for k, p in process._pipes.items():
408            if not p.direct:
409                os.close(p.child)
410                process.handles[k] = os.fdopen(p.parent, _fd_modes[k])
411       
412        # user doesn't need these
413        del process._pipes
414       
415        return process
416
417
418    def _as_child(self, process, gnupg_commands, args):
419        """Stuff run after forking in child"""
420        # child
421        for std in _stds:
422            p = process._pipes[std]
423            os.dup2( p.child, getattr(sys, "__%s__" % std).fileno() )
424       
425        for k, p in process._pipes.items():
426            if p.direct and k not in _stds:
427                # we want the fh to stay open after execing
428                fcntl.fcntl( p.child, fcntl.F_SETFD, 0 )
429       
430        fd_args = []
431       
432        for k, p in process._pipes.items():
433            # set command-line options for non-standard fds
434            if k not in _stds:
435                fd_args.extend([ _fd_options[k], "%d" % p.child ])
436           
437            if not p.direct: os.close(p.parent)
438       
439        command = [ self.call ] + fd_args + self.options.get_args() \
440                  + gnupg_commands + args
441
442        os.execvp( command[0], command )
443
444   
445class Pipe:
446    """simple struct holding stuff about pipes we use"""
447    def __init__(self, parent, child, direct):
448        self.parent = parent
449        self.child = child
450        self.direct = direct
451
452
453class Options:
454    """Objects of this class encompass options passed to GnuPG.
455    This class is responsible for determining command-line arguments
456    which are based on options.  It can be said that a GnuPG
457    object has-a Options object in its options attribute.
458   
459    Attributes which correlate directly to GnuPG options:
460   
461    Each option here defaults to false or None, and is described in
462    GnuPG documentation.
463   
464    Booleans (set these attributes to booleans)
465   
466      * armor
467      * no_greeting
468      * no_verbose
469      * quiet
470      * batch
471      * always_trust
472      * rfc1991
473      * openpgp
474      * force_v3_sigs
475      * no_options
476      * textmode
477   
478    Strings (set these attributes to strings)
479   
480      * homedir
481      * default_key
482      * comment
483      * compress_algo
484      * options
485   
486    Lists (set these attributes to lists)
487   
488      * recipients  (***NOTE*** plural of 'recipient')
489      * encrypt_to
490   
491    Meta options
492   
493    Meta options are options provided by this module that do
494    not correlate directly to any GnuPG option by name,
495    but are rather bundle of options used to accomplish
496    a specific goal, such as obtaining compatibility with PGP 5.
497    The actual arguments each of these reflects may change with time.  Each
498    defaults to false unless otherwise specified.
499   
500    meta_pgp_5_compatible -- If true, arguments are generated to try
501    to be compatible with PGP 5.x.
502     
503    meta_pgp_2_compatible -- If true, arguments are generated to try
504    to be compatible with PGP 2.x.
505   
506    meta_interactive -- If false, arguments are generated to try to
507    help the using program use GnuPG in a non-interactive
508    environment, such as CGI scripts.  Default is true.
509   
510    extra_args -- Extra option arguments may be passed in
511    via the attribute extra_args, a list.
512
513    >>> import GnuPGInterface
514    >>>
515    >>> gnupg = GnuPGInterface.GnuPG()
516    >>> gnupg.options.armor = 1
517    >>> gnupg.options.recipients = ['Alice', 'Bob']
518    >>> gnupg.options.extra_args = ['--no-secmem-warning']
519    >>>
520    >>> # no need for users to call this normally; just for show here
521    >>> gnupg.options.get_args()
522    ['--armor', '--recipient', 'Alice', '--recipient', 'Bob', '--no-secmem-warning']
523    """
524   
525    def __init__(self):
526        # booleans
527        self.armor = 0
528        self.no_greeting = 0
529        self.verbose = 0
530        self.no_verbose = 0
531        self.quiet = 0
532        self.batch = 0
533        self.always_trust = 0
534        self.rfc1991 = 0
535        self.openpgp = 0
536        self.force_v3_sigs = 0
537        self.no_options = 0
538        self.textmode = 0
539
540        # meta-option booleans
541        self.meta_pgp_5_compatible = 0
542        self.meta_pgp_2_compatible = 0
543        self.meta_interactive = 1
544
545        # strings
546        self.homedir = None
547        self.default_key = None
548        self.comment = None
549        self.compress_algo = None
550        self.options = None
551
552        # lists
553        self.encrypt_to = []
554        self.recipients = []
555       
556        # miscellaneous arguments
557        self.extra_args = []
558   
559    def get_args( self ):
560        """Generate a list of GnuPG arguments based upon attributes."""
561       
562        return self.get_meta_args() + self.get_standard_args() + self.extra_args
563
564    def get_standard_args( self ):
565        """Generate a list of standard, non-meta or extra arguments"""
566        args = []
567        if self.homedir != None: args.extend( [ '--homedir', self.homedir ] )
568        if self.options != None: args.extend( [ '--options', self.options ] )
569        if self.comment != None: args.extend( [ '--comment', self.comment ] )
570        if self.compress_algo != None: args.extend( [ '--compress-algo', self.compress_algo ] )
571        if self.default_key != None: args.extend( [ '--default-key', self.default_key ] )
572       
573        if self.no_options: args.append( '--no-options' )
574        if self.armor: args.append( '--armor' )
575        if self.textmode: args.append( '--textmode' )
576        if self.no_greeting: args.append( '--no-greeting' )
577        if self.verbose: args.append( '--verbose' )
578        if self.no_verbose: args.append( '--no-verbose' )
579        if self.quiet: args.append( '--quiet' )
580        if self.batch: args.append( '--batch' )
581        if self.always_trust: args.append( '--always-trust' )
582        if self.force_v3_sigs: args.append( '--force-v3-sigs' )
583        if self.rfc1991: args.append( '--rfc1991' )
584        if self.openpgp: args.append( '--openpgp' )
585
586        for r in self.recipients: args.extend( [ '--recipient',  r ] )
587        for r in self.encrypt_to: args.extend( [ '--encrypt-to', r ] )
588       
589        return args
590
591    def get_meta_args( self ):
592        """Get a list of generated meta-arguments"""
593        args = []
594
595        if self.meta_pgp_5_compatible: args.extend( [ '--compress-algo', '1',
596                                                      '--force-v3-sigs'
597                                                      ] )
598        if self.meta_pgp_2_compatible: args.append( '--rfc1991' )
599        if not self.meta_interactive: args.extend( [ '--batch', '--no-tty' ] )
600
601        return args
602
603
604class Process:
605    """Objects of this class encompass properties of a GnuPG
606    process spawned by GnuPG.run().
607   
608    # gnupg is a GnuPG object
609    process = gnupg.run( [ '--decrypt' ], stdout = 1 )
610    out = process.handles['stdout'].read()
611    ...
612    os.waitpid( process.pid, 0 )
613   
614    Data Attributes
615   
616    handles -- This is a map of filehandle-names to
617    the file handles, if any, that were requested via run() and hence
618    are connected to the running GnuPG process.  Valid names
619    of this map are only those handles that were requested.
620     
621    pid -- The PID of the spawned GnuPG process.
622    Useful to know, since once should call
623    os.waitpid() to clean up the process, especially
624    if multiple calls are made to run().
625    """
626   
627    def __init__(self):
628        self._pipes  = {}
629        self.handles = {}
630        self.pid     = None
631        self._waited = None
632
633    def wait(self):
634        """Wait on the process to exit, allowing for child cleanup.
635        Will raise an IOError if the process exits non-zero."""
636       
637        e = os.waitpid(self.pid, 0)[1]
638        if e != 0:
639            raise IOError, "GnuPG exited non-zero, with code %d" % (e << 8)
640
641def _run_doctests():
642    import doctest, GnuPGInterface
643    return doctest.testmod(GnuPGInterface)
644
645# deprecated
646GnuPGInterface = GnuPG
647
648if __name__ == '__main__':
649    _run_doctests()
Note: See TracBrowser for help on using the browser.