root/branches/gajim_0.9.1/src/common/GnuPGInterface.py

Revision 3838, 21.8 kB (checked in by nk, 3 years ago)

try to have OpenPGP working in Windoz

  • 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        if os.name == 'nt':   
273            self.call = 'gpg.exe'
274        else:   
275            self.call = 'gpg'
276        self.passphrase = None
277        self.options = Options()
278   
279    def run(self, gnupg_commands, args=None, create_fhs=None, attach_fhs=None):
280        """Calls GnuPG with the list of string commands gnupg_commands,
281        complete with prefixing dashes.
282        For example, gnupg_commands could be
283        '["--sign", "--encrypt"]'
284        Returns a GnuPGInterface.Process object.
285       
286        args is an optional list of GnuPG command arguments (not options),
287        such as keyID's to export, filenames to process, etc.
288
289        create_fhs is an optional list of GnuPG filehandle
290        names that will be set as keys of the returned Process object's
291        'handles' attribute.  The generated filehandles can be used
292        to communicate with GnuPG via standard input, standard output,
293        the status-fd, passphrase-fd, etc.
294       
295        Valid GnuPG filehandle names are:
296          * stdin
297          * stdout
298          * stderr
299          * status
300          * passphase
301          * command
302          * logger
303       
304        The purpose of each filehandle is described in the GnuPG
305        documentation.
306       
307        attach_fhs is an optional dictionary with GnuPG filehandle
308        names mapping to opened files.  GnuPG will read or write
309        to the file accordingly.  For example, if 'my_file' is an
310        opened file and 'attach_fhs[stdin] is my_file', then GnuPG
311        will read its standard input from my_file. This is useful
312        if you want GnuPG to read/write to/from an existing file.
313        For instance:
314       
315            f = open("encrypted.gpg")
316            gnupg.run(["--decrypt"], attach_fhs={'stdin': f})
317
318        Using attach_fhs also helps avoid system buffering
319        issues that can arise when using create_fhs, which
320        can cause the process to deadlock.
321       
322        If not mentioned in create_fhs or attach_fhs,
323        GnuPG filehandles which are a std* (stdin, stdout, stderr)
324        are defaulted to the running process' version of handle.
325        Otherwise, that type of handle is simply not used when calling GnuPG.
326        For example, if you do not care about getting data from GnuPG's
327        status filehandle, simply do not specify it.
328       
329        run() returns a Process() object which has a 'handles'
330        which is a dictionary mapping from the handle name
331        (such as 'stdin' or 'stdout') to the respective
332        newly-created FileObject connected to the running GnuPG process.
333        For instance, if the call was
334
335          process = gnupg.run(["--decrypt"], stdin=1)
336         
337        after run returns 'process.handles["stdin"]'
338        is a FileObject connected to GnuPG's standard input,
339        and can be written to.
340        """
341       
342        if args == None: args = []
343        if create_fhs == None: create_fhs = []
344        if attach_fhs == None: attach_fhs = {}
345       
346        for std in _stds:
347            if not attach_fhs.has_key(std) \
348               and std not in create_fhs:
349                attach_fhs.setdefault(std, getattr(sys, std))
350       
351        handle_passphrase = 0
352       
353        if self.passphrase != None \
354           and not attach_fhs.has_key('passphrase') \
355           and 'passphrase' not in create_fhs:
356            handle_passphrase = 1
357            create_fhs.append('passphrase')
358       
359        process = self._attach_fork_exec(gnupg_commands, args,
360                                         create_fhs, attach_fhs)
361       
362        if handle_passphrase:
363            passphrase_fh = process.handles['passphrase']
364            passphrase_fh.write( self.passphrase )
365            passphrase_fh.close()
366            del process.handles['passphrase']
367       
368        return process
369   
370   
371    def _attach_fork_exec(self, gnupg_commands, args, create_fhs, attach_fhs):
372        """This is like run(), but without the passphrase-helping
373        (note that run() calls this)."""
374       
375        process = Process()
376       
377        for fh_name in create_fhs + attach_fhs.keys():
378            if not _fd_modes.has_key(fh_name):
379                raise KeyError, \
380                      "unrecognized filehandle name '%s'; must be one of %s" \
381                      % (fh_name, _fd_modes.keys())
382
383        for fh_name in create_fhs:
384            # make sure the user doesn't specify a filehandle
385            # to be created *and* attached
386            if attach_fhs.has_key(fh_name):
387                raise ValueError, \
388                      "cannot have filehandle '%s' in both create_fhs and attach_fhs" \
389                      % fh_name
390
391            pipe = os.pipe()
392            # fix by drt@un.bewaff.net noting
393            # that since pipes are unidirectional on some systems,
394            # so we have to 'turn the pipe around'
395            # if we are writing
396            if _fd_modes[fh_name] == 'w': pipe = (pipe[1], pipe[0])
397            process._pipes[fh_name] = Pipe(pipe[0], pipe[1], 0)
398       
399        for fh_name, fh in attach_fhs.items():
400            process._pipes[fh_name] = Pipe(fh.fileno(), fh.fileno(), 1)
401       
402        process.pid = os.fork()
403       
404        if process.pid == 0: self._as_child(process, gnupg_commands, args)
405        return self._as_parent(process)
406   
407   
408    def _as_parent(self, process):
409        """Stuff run after forking in parent"""
410        for k, p in process._pipes.items():
411            if not p.direct:
412                os.close(p.child)
413                process.handles[k] = os.fdopen(p.parent, _fd_modes[k])
414       
415        # user doesn't need these
416        del process._pipes
417       
418        return process
419
420
421    def _as_child(self, process, gnupg_commands, args):
422        """Stuff run after forking in child"""
423        # child
424        for std in _stds:
425            p = process._pipes[std]
426            os.dup2( p.child, getattr(sys, "__%s__" % std).fileno() )
427       
428        for k, p in process._pipes.items():
429            if p.direct and k not in _stds:
430                # we want the fh to stay open after execing
431                fcntl.fcntl( p.child, fcntl.F_SETFD, 0 )
432       
433        fd_args = []
434       
435        for k, p in process._pipes.items():
436            # set command-line options for non-standard fds
437            if k not in _stds:
438                fd_args.extend([ _fd_options[k], "%d" % p.child ])
439           
440            if not p.direct: os.close(p.parent)
441       
442        command = [ self.call ] + fd_args + self.options.get_args() \
443                  + gnupg_commands + args
444
445        os.execvp( command[0], command )
446
447   
448class Pipe:
449    """simple struct holding stuff about pipes we use"""
450    def __init__(self, parent, child, direct):
451        self.parent = parent
452        self.child = child
453        self.direct = direct
454
455
456class Options:
457    """Objects of this class encompass options passed to GnuPG.
458    This class is responsible for determining command-line arguments
459    which are based on options.  It can be said that a GnuPG
460    object has-a Options object in its options attribute.
461   
462    Attributes which correlate directly to GnuPG options:
463   
464    Each option here defaults to false or None, and is described in
465    GnuPG documentation.
466   
467    Booleans (set these attributes to booleans)
468   
469      * armor
470      * no_greeting
471      * no_verbose
472      * quiet
473      * batch
474      * always_trust
475      * rfc1991
476      * openpgp
477      * force_v3_sigs
478      * no_options
479      * textmode
480   
481    Strings (set these attributes to strings)
482   
483      * homedir
484      * default_key
485      * comment
486      * compress_algo
487      * options
488   
489    Lists (set these attributes to lists)
490   
491      * recipients  (***NOTE*** plural of 'recipient')
492      * encrypt_to
493   
494    Meta options
495   
496    Meta options are options provided by this module that do
497    not correlate directly to any GnuPG option by name,
498    but are rather bundle of options used to accomplish
499    a specific goal, such as obtaining compatibility with PGP 5.
500    The actual arguments each of these reflects may change with time.  Each
501    defaults to false unless otherwise specified.
502   
503    meta_pgp_5_compatible -- If true, arguments are generated to try
504    to be compatible with PGP 5.x.
505     
506    meta_pgp_2_compatible -- If true, arguments are generated to try
507    to be compatible with PGP 2.x.
508   
509    meta_interactive -- If false, arguments are generated to try to
510    help the using program use GnuPG in a non-interactive
511    environment, such as CGI scripts.  Default is true.
512   
513    extra_args -- Extra option arguments may be passed in
514    via the attribute extra_args, a list.
515
516    >>> import GnuPGInterface
517    >>>
518    >>> gnupg = GnuPGInterface.GnuPG()
519    >>> gnupg.options.armor = 1
520    >>> gnupg.options.recipients = ['Alice', 'Bob']
521    >>> gnupg.options.extra_args = ['--no-secmem-warning']
522    >>>
523    >>> # no need for users to call this normally; just for show here
524    >>> gnupg.options.get_args()
525    ['--armor', '--recipient', 'Alice', '--recipient', 'Bob', '--no-secmem-warning']
526    """
527   
528    def __init__(self):
529        # booleans
530        self.armor = 0
531        self.no_greeting = 0
532        self.verbose = 0
533        self.no_verbose = 0
534        self.quiet = 0
535        self.batch = 0
536        self.always_trust = 0
537        self.rfc1991 = 0
538        self.openpgp = 0
539        self.force_v3_sigs = 0
540        self.no_options = 0
541        self.textmode = 0
542
543        # meta-option booleans
544        self.meta_pgp_5_compatible = 0
545        self.meta_pgp_2_compatible = 0
546        self.meta_interactive = 1
547
548        # strings
549        self.homedir = None
550        self.default_key = None
551        self.comment = None
552        self.compress_algo = None
553        self.options = None
554
555        # lists
556        self.encrypt_to = []
557        self.recipients = []
558       
559        # miscellaneous arguments
560        self.extra_args = []
561   
562    def get_args( self ):
563        """Generate a list of GnuPG arguments based upon attributes."""
564       
565        return self.get_meta_args() + self.get_standard_args() + self.extra_args
566
567    def get_standard_args( self ):
568        """Generate a list of standard, non-meta or extra arguments"""
569        args = []
570        if self.homedir != None: args.extend( [ '--homedir', self.homedir ] )
571        if self.options != None: args.extend( [ '--options', self.options ] )
572        if self.comment != None: args.extend( [ '--comment', self.comment ] )
573        if self.compress_algo != None: args.extend( [ '--compress-algo', self.compress_algo ] )
574        if self.default_key != None: args.extend( [ '--default-key', self.default_key ] )
575       
576        if self.no_options: args.append( '--no-options' )
577        if self.armor: args.append( '--armor' )
578        if self.textmode: args.append( '--textmode' )
579        if self.no_greeting: args.append( '--no-greeting' )
580        if self.verbose: args.append( '--verbose' )
581        if self.no_verbose: args.append( '--no-verbose' )
582        if self.quiet: args.append( '--quiet' )
583        if self.batch: args.append( '--batch' )
584        if self.always_trust: args.append( '--always-trust' )
585        if self.force_v3_sigs: args.append( '--force-v3-sigs' )
586        if self.rfc1991: args.append( '--rfc1991' )
587        if self.openpgp: args.append( '--openpgp' )
588
589        for r in self.recipients: args.extend( [ '--recipient',  r ] )
590        for r in self.encrypt_to: args.extend( [ '--encrypt-to', r ] )
591       
592        return args
593
594    def get_meta_args( self ):
595        """Get a list of generated meta-arguments"""
596        args = []
597
598        if self.meta_pgp_5_compatible: args.extend( [ '--compress-algo', '1',
599                                                      '--force-v3-sigs'
600                                                      ] )
601        if self.meta_pgp_2_compatible: args.append( '--rfc1991' )
602        if not self.meta_interactive: args.extend( [ '--batch', '--no-tty' ] )
603
604        return args
605
606
607class Process:
608    """Objects of this class encompass properties of a GnuPG
609    process spawned by GnuPG.run().
610   
611    # gnupg is a GnuPG object
612    process = gnupg.run( [ '--decrypt' ], stdout = 1 )
613    out = process.handles['stdout'].read()
614    ...
615    os.waitpid( process.pid, 0 )
616   
617    Data Attributes
618   
619    handles -- This is a map of filehandle-names to
620    the file handles, if any, that were requested via run() and hence
621    are connected to the running GnuPG process.  Valid names
622    of this map are only those handles that were requested.
623     
624    pid -- The PID of the spawned GnuPG process.
625    Useful to know, since once should call
626    os.waitpid() to clean up the process, especially
627    if multiple calls are made to run().
628    """
629   
630    def __init__(self):
631        self._pipes  = {}
632        self.handles = {}
633        self.pid     = None
634        self._waited = None
635
636    def wait(self):
637        """Wait on the process to exit, allowing for child cleanup.
638        Will raise an IOError if the process exits non-zero."""
639       
640        e = os.waitpid(self.pid, 0)[1]
641        if e != 0:
642            raise IOError, "GnuPG exited non-zero, with code %d" % (e << 8)
643
644def _run_doctests():
645    import doctest, GnuPGInterface
646    return doctest.testmod(GnuPGInterface)
647
648# deprecated
649GnuPGInterface = GnuPG
650
651if __name__ == '__main__':
652    _run_doctests()
Note: See TracBrowser for help on using the browser.