Back to index

obnam  1.1
encryption.py
Go to the documentation of this file.
00001 # Copyright 2011  Lars Wirzenius
00002 # 
00003 # This program is free software: you can redistribute it and/or modify
00004 # it under the terms of the GNU General Public License as published by
00005 # the Free Software Foundation, either version 3 of the License, or
00006 # (at your option) any later version.
00007 # 
00008 # This program is distributed in the hope that it will be useful,
00009 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00010 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00011 # GNU General Public License for more details.
00012 # 
00013 # You should have received a copy of the GNU General Public License
00014 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
00015 
00016 
00017 import os
00018 import shutil
00019 import subprocess
00020 import tempfile
00021 import tracing
00022 
00023 import obnamlib
00024 
00025 
00026 def generate_symmetric_key(numbits, filename='/dev/random'):
00027     '''Generate a random key of at least numbits for symmetric encryption.'''
00028 
00029     tracing.trace('numbits=%d', numbits)
00030     
00031     bytes = (numbits + 7) / 8
00032     f = open(filename, 'rb')
00033     key = f.read(bytes)
00034     f.close()
00035     
00036     return key.encode('hex')
00037 
00038 
00039 class SymmetricKeyCache(object):
00040 
00041     '''Cache symmetric keys in memory.'''
00042     
00043     def __init__(self):
00044         self.clear()
00045     
00046     def get(self, repo, toplevel):
00047         if repo in self.repos and toplevel in self.repos[repo]:
00048             return self.repos[repo][toplevel]
00049         return None
00050         
00051     def put(self, repo, toplevel, key):
00052         if repo not in self.repos:
00053             self.repos[repo] = {}
00054         self.repos[repo][toplevel] = key
00055         
00056     def clear(self):
00057         self.repos = {}
00058     
00059     
00060 def _gpg_pipe(args, data, passphrase):
00061     '''Pipe things through gpg.
00062     
00063     With the right args, this can be either an encryption or a decryption
00064     operation.
00065     
00066     For safety, we give the passphrase to gpg via a file descriptor.
00067     The argument list is modified to include the relevant options for that.
00068     
00069     The data is fed to gpg via a temporary file, readable only by
00070     the owner, to avoid congested pipes.
00071     
00072     '''
00073     
00074     # Open pipe for passphrase, and write it there. If passphrase is
00075     # very long (more than 4 KiB by default), this might block. A better
00076     # implementation would be to have a loop around select(2) to do pipe
00077     # I/O when it can be done without blocking. Patches most welcome.
00078 
00079     keypipe = os.pipe()
00080     os.write(keypipe[1], passphrase + '\n')
00081     os.close(keypipe[1])
00082     
00083     # Actually run gpg.
00084     
00085     argv = ['gpg', '--passphrase-fd', str(keypipe[0]), '-q', '--batch'] + args
00086     tracing.trace('argv=%s', repr(argv))
00087     p = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
00088                          stderr=subprocess.PIPE)
00089     out, err = p.communicate(data)
00090     
00091     os.close(keypipe[0])
00092     
00093     # Return output data, or deal with errors.
00094     if p.returncode: # pragma: no cover
00095         raise obnamlib.Error(err)
00096         
00097     return out
00098     
00099     
00100 def encrypt_symmetric(cleartext, key):
00101     '''Encrypt data with symmetric encryption.'''
00102     return _gpg_pipe(['-c'], cleartext, key)
00103     
00104     
00105 def decrypt_symmetric(encrypted, key):
00106     '''Decrypt encrypted data with symmetric encryption.'''
00107     return _gpg_pipe(['-d'], encrypted, key)
00108 
00109 
00110 def _gpg(args, stdin='', gpghome=None):
00111     '''Run gpg and return its output.'''
00112     
00113     env = dict()
00114     env.update(os.environ)
00115     if gpghome is not None:
00116         env['GNUPGHOME'] = gpghome
00117         tracing.trace('gpghome=%s' % gpghome)
00118     
00119     argv = ['gpg', '-q', '--batch'] + args
00120     tracing.trace('argv=%s', repr(argv))
00121     p = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
00122                          stderr=subprocess.PIPE, env=env)
00123     out, err = p.communicate(stdin)
00124     
00125     # Return output data, or deal with errors.
00126     if p.returncode: # pragma: no cover
00127         raise obnamlib.Error(err)
00128         
00129     return out
00130 
00131 
00132 def get_public_key(keyid, gpghome=None):
00133     '''Return the ASCII armored export form of a given public key.'''
00134     return _gpg(['--export', '--armor', keyid], gpghome=gpghome)
00135 
00136 
00137 
00138 class Keyring(object):
00139 
00140     '''A simplistic representation of GnuPG keyrings.
00141     
00142     Just enough functionality for obnam's purposes.
00143     
00144     '''
00145     
00146     _keyring_name = 'pubring.gpg'
00147     
00148     def __init__(self, encoded=''):
00149         self._encoded = encoded
00150         self._gpghome = None
00151         self._keyids = None
00152         
00153     def _setup(self):
00154         self._gpghome = tempfile.mkdtemp()
00155         f = open(self._keyring, 'wb')
00156         f.write(self._encoded)
00157         f.close()
00158         
00159     def _cleanup(self):
00160         shutil.rmtree(self._gpghome)
00161         self._gpghome = None
00162         
00163     @property
00164     def _keyring(self):
00165         return os.path.join(self._gpghome, self._keyring_name)
00166         
00167     def _real_keyids(self):
00168         output = self.gpg(False, ['--list-keys', '--with-colons'])
00169 
00170         keyids = []
00171         for line in output.splitlines():
00172             fields = line.split(':')
00173             if len(fields) >= 5 and fields[0] == 'pub':
00174                 keyids.append(fields[4])
00175         return keyids
00176         
00177     def keyids(self):
00178         if self._keyids is None:
00179             self._keyids = self._real_keyids()
00180         return self._keyids
00181         
00182     def __str__(self):
00183         return self._encoded
00184         
00185     def __contains__(self, keyid):
00186         return keyid in self.keyids()
00187         
00188     def _reread_keyring(self):
00189         f = open(self._keyring, 'rb')
00190         self._encoded = f.read()
00191         f.close()
00192         self._keyids = None
00193         
00194     def add(self, key):
00195         self.gpg(True, ['--import'], stdin=key)
00196         
00197     def remove(self, keyid):
00198         self.gpg(True, ['--delete-key', '--yes', keyid])
00199 
00200     def gpg(self, reread, *args, **kwargs):
00201         self._setup()
00202         kwargs['gpghome'] = self._gpghome
00203         try:
00204             result = _gpg(*args, **kwargs)
00205         except BaseException: # pragma: no cover
00206             self._cleanup()
00207             raise
00208         else:
00209             if reread:
00210                 self._reread_keyring()
00211             self._cleanup()
00212             return result
00213 
00214 
00215 class SecretKeyring(Keyring):
00216 
00217     '''Same as Keyring, but for secret keys.'''
00218     
00219     _keyring_name = 'secring.gpg'
00220 
00221     def _real_keyids(self):
00222         output = self.gpg(False, ['--list-secret-keys', '--with-colons'])
00223 
00224         keyids = []
00225         for line in output.splitlines():
00226             fields = line.split(':')
00227             if len(fields) >= 5 and fields[0] == 'sec':
00228                 keyids.append(fields[4])
00229         return keyids
00230         
00231 
00232 def encrypt_with_keyring(cleartext, keyring):
00233     '''Encrypt data with all keys in a keyring.'''
00234     recipients = []
00235     for keyid in keyring.keyids():
00236         recipients += ['-r', keyid]
00237     return keyring.gpg(False, 
00238                         ['-e', 
00239                          '--trust-model', 'always',
00240                          '--no-encrypt-to',
00241                          '--no-default-recipient',
00242                             ] + recipients,
00243                        stdin=cleartext)
00244     
00245     
00246 def decrypt_with_secret_keys(encrypted, gpghome=None):
00247     '''Decrypt data using secret keys GnuPG finds on its own.'''
00248     return _gpg(['-d'], stdin=encrypted, gpghome=gpghome)
00249