Back to index

obnam  1.1
encryption_plugin.py
Go to the documentation of this file.
00001 # Copyright (C) 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 logging
00018 import os
00019 
00020 import obnamlib
00021 
00022 
00023 class EncryptionPlugin(obnamlib.ObnamPlugin):
00024 
00025     def enable(self):
00026         encryption_group = obnamlib.option_group['encryption'] = 'Encryption'
00027 
00028         self.app.settings.string(['encrypt-with'],
00029                                    'PGP key with which to encrypt data '
00030                                         'in the backup repository',
00031                                  group=encryption_group)
00032         self.app.settings.string(['keyid'],
00033                                    'PGP key id to add to/remove from '
00034                                         'the backup repository',
00035                                  group=encryption_group)
00036         self.app.settings.boolean(['weak-random'],
00037                                     'use /dev/urandom instead of /dev/random '
00038                                         'to generate symmetric keys',
00039                                  group=encryption_group)
00040         self.app.settings.string(['symmetric-key-bits'],
00041                                    'size of symmetric key, in bits',
00042                                  group=encryption_group)
00043         
00044         self.tag = "encrypt1"
00045 
00046         hooks = [
00047             ('repository-toplevel-init', self.toplevel_init,
00048              obnamlib.Hook.DEFAULT_PRIORITY),
00049             ('repository-data', self,
00050              obnamlib.Hook.LATE_PRIORITY),
00051             ('repository-add-client', self.add_client,
00052              obnamlib.Hook.DEFAULT_PRIORITY),
00053         ]
00054         for name, callback, rev in hooks:
00055             self.app.hooks.add_callback(name, callback, rev)
00056             
00057         self._pubkey = None
00058         
00059         self.app.add_subcommand('client-keys', self.client_keys)
00060         self.app.add_subcommand('list-keys', self.list_keys)
00061         self.app.add_subcommand('list-toplevels', self.list_toplevels)
00062         self.app.add_subcommand('add-key', self.add_key)
00063         self.app.add_subcommand('remove-key', self.remove_key)
00064         self.app.add_subcommand('remove-client', self.remove_client,
00065                                 arg_synopsis='[CLIENT-NAME]...')
00066         
00067         self._symkeys = obnamlib.SymmetricKeyCache()
00068         
00069     def disable(self):
00070         self._symkeys.clear()
00071 
00072     @property
00073     def keyid(self):
00074         return self.app.settings['encrypt-with']
00075         
00076     @property
00077     def pubkey(self):
00078         if self._pubkey is None:
00079             self._pubkey = obnamlib.get_public_key(self.keyid)
00080         return self._pubkey
00081         
00082     @property
00083     def devrandom(self):
00084         if self.app.settings['weak-random']:
00085             return '/dev/urandom'
00086         else:
00087             return '/dev/random'
00088 
00089     @property
00090     def symmetric_key_bits(self):
00091         return int(self.app.settings['symmetric-key-bits'] or '256')
00092 
00093     def _write_file(self, repo, pathname, contents):
00094         repo.fs.fs.write_file(pathname, contents)
00095 
00096     def _overwrite_file(self, repo, pathname, contents):
00097         repo.fs.fs.overwrite_file(pathname, contents)
00098 
00099     def toplevel_init(self, repo, toplevel):
00100         '''Initialize a new toplevel for encryption.'''
00101         
00102         if not self.keyid:
00103             return
00104         
00105         pubkeys = obnamlib.Keyring()
00106         pubkeys.add(self.pubkey)
00107 
00108         symmetric_key = obnamlib.generate_symmetric_key(
00109                                 self.symmetric_key_bits,
00110                                 filename=self.devrandom)
00111         encrypted = obnamlib.encrypt_with_keyring(symmetric_key, pubkeys)
00112         self._write_file(repo, os.path.join(toplevel, 'key'), encrypted)
00113 
00114         encoded = str(pubkeys)
00115         encrypted = obnamlib.encrypt_symmetric(encoded, symmetric_key)
00116         self._write_file(repo, os.path.join(toplevel, 'userkeys'), encrypted)
00117 
00118     def filter_read(self, encrypted, repo, toplevel):
00119         if not self.keyid:
00120             return encrypted
00121         symmetric_key = self.get_symmetric_key(repo, toplevel)
00122         return obnamlib.decrypt_symmetric(encrypted, symmetric_key)
00123 
00124     def filter_write(self, cleartext, repo, toplevel):
00125         if not self.keyid:
00126             return cleartext
00127         symmetric_key = self.get_symmetric_key(repo, toplevel)
00128         return obnamlib.encrypt_symmetric(cleartext, symmetric_key)
00129 
00130     def get_symmetric_key(self, repo, toplevel):
00131         key = self._symkeys.get(repo, toplevel)
00132         if key is None:
00133             encoded = repo.fs.fs.cat(os.path.join(toplevel, 'key'))
00134             key = obnamlib.decrypt_with_secret_keys(encoded)
00135             self._symkeys.put(repo, toplevel, key)
00136         return key
00137 
00138     def read_keyring(self, repo, toplevel):
00139         encrypted = repo.fs.fs.cat(os.path.join(toplevel, 'userkeys'))
00140         encoded = self.filter_read(encrypted, repo, toplevel)
00141         return obnamlib.Keyring(encoded=encoded)
00142 
00143     def write_keyring(self, repo, toplevel, keyring):
00144         encoded = str(keyring)
00145         encrypted = self.filter_write(encoded, repo, toplevel)
00146         pathname = os.path.join(toplevel, 'userkeys')
00147         self._overwrite_file(repo, pathname, encrypted)
00148 
00149     def add_to_userkeys(self, repo, toplevel, public_key):
00150         userkeys = self.read_keyring(repo, toplevel)
00151         userkeys.add(public_key)
00152         self.write_keyring(repo, toplevel, userkeys)
00153 
00154     def remove_from_userkeys(self, repo, toplevel, keyid):
00155         userkeys = self.read_keyring(repo, toplevel)
00156         if keyid in userkeys:
00157             logging.debug('removing key %s from %s' % (keyid, toplevel))
00158             userkeys.remove(keyid)
00159             self.write_keyring(repo, toplevel, userkeys)
00160         else:
00161             logging.debug('unable to remove key %s from %s (not there)' %
00162                           (keyid, toplevel))
00163 
00164     def add_client(self, clientlist, client_name):
00165         clientlist.set_client_keyid(client_name, self.keyid)
00166 
00167     def quit_if_unencrypted(self):
00168         if self.app.settings['encrypt-with']:
00169             return False
00170         self.app.output.write('Warning: Encryption not in use.\n')
00171         self.app.output.write('(Use --encrypt-with to set key.)\n')
00172         return True
00173 
00174     def client_keys(self, args):
00175         '''List clients and their keys in the repository.'''
00176         if self.quit_if_unencrypted():
00177             return
00178         repo = self.app.open_repository()
00179         clients = repo.list_clients()
00180         for client in clients:
00181             keyid = repo.clientlist.get_client_keyid(client)
00182             if keyid is None:
00183                 keyid = 'no key'
00184             print client, keyid
00185 
00186     def _find_keys_and_toplevels(self, repo):
00187         toplevels = repo.fs.listdir('.')
00188         keys = dict()
00189         tops = dict()
00190         for toplevel in [d for d in toplevels if d != 'metadata']:
00191             userkeys = self.read_keyring(repo, toplevel)
00192             for keyid in userkeys.keyids():
00193                 keys[keyid] = keys.get(keyid, []) + [toplevel]
00194                 tops[toplevel] = tops.get(toplevel, []) + [keyid]
00195         return keys, tops
00196 
00197     def list_keys(self, args):
00198         '''List keys and the repository toplevels they're used in.'''
00199         if self.quit_if_unencrypted():
00200             return
00201         repo = self.app.open_repository()
00202         keys, tops = self._find_keys_and_toplevels(repo)
00203         for keyid in keys:
00204             print 'key: %s' % keyid
00205             for toplevel in keys[keyid]:
00206                 print '  %s' % toplevel
00207 
00208     def list_toplevels(self, args):
00209         '''List repository toplevel directories and their keys.'''
00210         if self.quit_if_unencrypted():
00211             return
00212         repo = self.app.open_repository()
00213         keys, tops = self._find_keys_and_toplevels(repo)
00214         for toplevel in tops:
00215             print 'toplevel: %s' % toplevel
00216             for keyid in tops[toplevel]:
00217                 print '  %s' % keyid
00218 
00219     _shared = ['chunklist', 'chunks', 'chunksums', 'clientlist']
00220     
00221     def _find_clientdirs(self, repo, client_names):
00222         return [repo.client_dir(repo.clientlist.get_client_id(x))
00223                  for x in client_names]
00224 
00225     def add_key(self, args):
00226         '''Add a key to the repository.'''
00227         if self.quit_if_unencrypted():
00228             return
00229         self.app.settings.require('keyid')
00230         repo = self.app.open_repository()
00231         keyid = self.app.settings['keyid']
00232         key = obnamlib.get_public_key(keyid)
00233         clients = self._find_clientdirs(repo, args)
00234         for toplevel in self._shared + clients:
00235             self.add_to_userkeys(repo, toplevel, key)
00236 
00237     def remove_key(self, args):
00238         '''Remove a key from the repository.'''
00239         if self.quit_if_unencrypted():
00240             return
00241         self.app.settings.require('keyid')
00242         repo = self.app.open_repository()
00243         keyid = self.app.settings['keyid']
00244         clients = self._find_clientdirs(repo, args)
00245         for toplevel in self._shared + clients:
00246             self.remove_from_userkeys(repo, toplevel, keyid)
00247 
00248     def remove_client(self, args):
00249         '''Remove client and its key from repository.'''
00250         if self.quit_if_unencrypted():
00251             return
00252         repo = self.app.open_repository()
00253         repo.lock_root()
00254         for client_name in args:
00255             logging.info('removing client %s' % client_name)
00256             repo.remove_client(client_name)
00257         repo.commit_root()
00258