CedarBackup3.extend.encrypt

Package CedarBackup3 :: Package extend :: Module encrypt |
[hide private]
[frames] | no frames]

# Source Code for Module CedarBackup3.extend.encrypt

      1  # -*- coding: iso-8859-1 -*-
      2  # vim: set ft=python ts=3 sw=3 expandtab:
      3  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
      4  #
      5  #              C E D A R
      6  #          S O L U T I O N S       "Software done right."
      7  #           S O F T W A R E
      8  #
      9  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
     10  #
     11  # Copyright (c) 2007,2010,2015 Kenneth J. Pronovici.
     12  # All rights reserved.
     13  #
     14  # This program is free software; you can redistribute it and/or
     15  # modify it under the terms of the GNU General Public License,
     16  # Version 2, as published by the Free Software Foundation.
     17  #
     18  # This program is distributed in the hope that it will be useful,
     19  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     20  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
     21  #
     22  # Copies of the GNU General Public License are available from
     23  # the Free Software Foundation website, http://www.gnu.org/.
     24  #
     25  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
     26  #
     27  # Author   : Kenneth J. Pronovici <pronovic@ieee.org>
     28  # Language : Python 3 (>= 3.4)
     29  # Project  : Official Cedar Backup Extensions
     30  # Purpose  : Provides an extension to encrypt staging directories.
     31  #
     32  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
     33
     34  ########################################################################
     35  # Module documentation
     36  ########################################################################
     37
     38  """
     39  Provides an extension to encrypt staging directories.
     40
     41  When this extension is executed, all backed-up files in the configured Cedar
     42  Backup staging directory will be encrypted using gpg.  Any directory which has
     43  already been encrypted (as indicated by the C{cback.encrypt} file) will be
     44  ignored.
     45
     46  This extension requires a new configuration section <encrypt> and is intended
     47  to be run immediately after the standard stage action or immediately before the
     48  standard store action.  Aside from its own configuration, it requires the
     49  options and staging configuration sections in the standard Cedar Backup
     50  configuration file.
     51
     52  @author: Kenneth J. Pronovici <pronovic@ieee.org>
     53  """
     54
     55  ########################################################################
     56  # Imported modules
     57  ########################################################################
     58
     59  # System modules
     60  import os
     61  import logging
     62  from functools import total_ordering
     63
     64  # Cedar Backup modules
     65  from CedarBackup3.util import resolveCommand, executeCommand, changeOwnership
     66  from CedarBackup3.xmlutil import createInputDom, addContainerNode, addStringNode
     67  from CedarBackup3.xmlutil import readFirstChild, readString
     68  from CedarBackup3.actions.util import findDailyDirs, writeIndicatorFile, getBackupFiles
     69
     70
     71  ########################################################################
     72  # Module-wide constants and variables
     73  ########################################################################
     74
     75  logger = logging.getLogger("CedarBackup3.log.extend.encrypt")
     76
     77  GPG_COMMAND = [ "gpg", ]
     78  VALID_ENCRYPT_MODES = [ "gpg", ]
     79  ENCRYPT_INDICATOR = "cback.encrypt"
     80
     81
     82  ########################################################################
     83  # EncryptConfig class definition
     84  ########################################################################
     85
     86  @total_ordering
     87 -class EncryptConfig(object):
     88
     89     """
     90     Class representing encrypt configuration.
     91
     92     Encrypt configuration is used for encrypting staging directories.
     93
     94     The following restrictions exist on data in this class:
     95
     96        - The encrypt mode must be one of the values in L{VALID_ENCRYPT_MODES}
     97        - The encrypt target value must be a non-empty string
     98
     99     @sort: __init__, __repr__, __str__, __cmp__, __eq__, __lt__, __gt__,
    100           encryptMode, encryptTarget
    101     """
    102
    103 -   def __init__(self, encryptMode=None, encryptTarget=None):
    104        """
    105        Constructor for the C{EncryptConfig} class.
    106
    107        @param encryptMode: Encryption mode
    108        @param encryptTarget: Encryption target (for instance, GPG recipient)
    109
    110        @raise ValueError: If one of the values is invalid.
    111        """
    112        self._encryptMode = None
    113        self._encryptTarget = None
    114        self.encryptMode = encryptMode
    115        self.encryptTarget = encryptTarget
    116
    117 -   def __repr__(self):
    118        """
    119        Official string representation for class instance.
    120        """
    121        return "EncryptConfig(%s, %s)" % (self.encryptMode, self.encryptTarget)
    122
    123 -   def __str__(self):
    124        """
    125        Informal string representation for class instance.
    126        """
    127        return self.__repr__()
    128
    129 -   def __eq__(self, other):
    130        """Equals operator, iplemented in terms of original Python 2 compare operator."""
    131        return self.__cmp__(other) == 0
    132
    133 -   def __lt__(self, other):
    134        """Less-than operator, iplemented in terms of original Python 2 compare operator."""
    135        return self.__cmp__(other) < 0
    136
    137 -   def __gt__(self, other):
    138        """Greater-than operator, iplemented in terms of original Python 2 compare operator."""
    139        return self.__cmp__(other) > 0
    140
    141 -   def __cmp__(self, other):
    142        """
    143        Original Python 2 comparison operator.
    144        Lists within this class are "unordered" for equality comparisons.
    145        @param other: Other object to compare to.
    146        @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
    147        """
    148        if other is None:
    149           return 1
    150        if self.encryptMode != other.encryptMode:
    151           if str(self.encryptMode or "") < str(other.encryptMode or ""):
    152              return -1
    153           else:
    154              return 1
    155        if self.encryptTarget != other.encryptTarget:
    156           if str(self.encryptTarget or "") < str(other.encryptTarget or ""):
    157              return -1
    158           else:
    159              return 1
    160        return 0
    161
    162 -   def _setEncryptMode(self, value):
    163        """
    164        Property target used to set the encrypt mode.
    165        If not C{None}, the mode must be one of the values in L{VALID_ENCRYPT_MODES}.
    166        @raise ValueError: If the value is not valid.
    167        """
    168        if value is not None:
    169           if value not in VALID_ENCRYPT_MODES:
    170              raise ValueError("Encrypt mode must be one of %s." % VALID_ENCRYPT_MODES)
    171        self._encryptMode = value
    172
    173 -   def _getEncryptMode(self):
    174        """
    175        Property target used to get the encrypt mode.
    176        """
    177        return self._encryptMode
    178
    179 -   def _setEncryptTarget(self, value):
    180        """
    181        Property target used to set the encrypt target.
    182        """
    183        if value is not None:
    184           if len(value) < 1:
    185              raise ValueError("Encrypt target must be non-empty string.")
    186        self._encryptTarget = value
    187
    188 -   def _getEncryptTarget(self):
    189        """
    190        Property target used to get the encrypt target.
    191        """
    192        return self._encryptTarget
    193
    194     encryptMode = property(_getEncryptMode, _setEncryptMode, None, doc="Encrypt mode.")
    195     encryptTarget = property(_getEncryptTarget, _setEncryptTarget, None, doc="Encrypt target (i.e. GPG recipient).")
    196
    197
    198  ########################################################################
    199  # LocalConfig class definition
    200  ########################################################################
    201
    202  @total_ordering
    203 -class LocalConfig(object):
    204
    205     """
    206     Class representing this extension's configuration document.
    207
    208     This is not a general-purpose configuration object like the main Cedar
    209     Backup configuration object.  Instead, it just knows how to parse and emit
    210     encrypt-specific configuration values.  Third parties who need to read and
    211     write configuration related to this extension should access it through the
    212     constructor, C{validate} and C{addConfig} methods.
    213
    214     @note: Lists within this class are "unordered" for equality comparisons.
    215
    216     @sort: __init__, __repr__, __str__, __cmp__, __eq__, __lt__, __gt__,
    217           encrypt, validate, addConfig
    218     """
    219
    220 -   def __init__(self, xmlData=None, xmlPath=None, validate=True):
    221        """
    222        Initializes a configuration object.
    223
    224        If you initialize the object without passing either C{xmlData} or
    225        C{xmlPath} then configuration will be empty and will be invalid until it
    226        is filled in properly.
    227
    228        No reference to the original XML data or original path is saved off by
    229        this class.  Once the data has been parsed (successfully or not) this
    230        original information is discarded.
    231
    232        Unless the C{validate} argument is C{False}, the L{LocalConfig.validate}
    233        method will be called (with its default arguments) against configuration
    234        after successfully parsing any passed-in XML.  Keep in mind that even if
    235        C{validate} is C{False}, it might not be possible to parse the passed-in
    236        XML document if lower-level validations fail.
    237
    238        @note: It is strongly suggested that the C{validate} option always be set
    239        to C{True} (the default) unless there is a specific need to read in
    240        invalid configuration from disk.
    241
    242        @param xmlData: XML data representing configuration.
    243        @type xmlData: String data.
    244
    245        @param xmlPath: Path to an XML file on disk.
    246        @type xmlPath: Absolute path to a file on disk.
    247
    248        @param validate: Validate the document after parsing it.
    249        @type validate: Boolean true/false.
    250
    251        @raise ValueError: If both C{xmlData} and C{xmlPath} are passed-in.
    252        @raise ValueError: If the XML data in C{xmlData} or C{xmlPath} cannot be parsed.
    253        @raise ValueError: If the parsed configuration document is not valid.
    254        """
    255        self._encrypt = None
    256        self.encrypt = None
    257        if xmlData is not None and xmlPath is not None:
    258           raise ValueError("Use either xmlData or xmlPath, but not both.")
    259        if xmlData is not None:
    260           self._parseXmlData(xmlData)
    261           if validate:
    262              self.validate()
    263        elif xmlPath is not None:
    264           with open(xmlPath) as f:
    265              xmlData = f.read()
    266           self._parseXmlData(xmlData)
    267           if validate:
    268              self.validate()
    269
    270 -   def __repr__(self):
    271        """
    272        Official string representation for class instance.
    273        """
    274        return "LocalConfig(%s)" % (self.encrypt)
    275
    276 -   def __str__(self):
    277        """
    278        Informal string representation for class instance.
    279        """
    280        return self.__repr__()
    281
    282 -   def __eq__(self, other):
    283        """Equals operator, iplemented in terms of original Python 2 compare operator."""
    284        return self.__cmp__(other) == 0
    285
    286 -   def __lt__(self, other):
    287        """Less-than operator, iplemented in terms of original Python 2 compare operator."""
    288        return self.__cmp__(other) < 0
    289
    290 -   def __gt__(self, other):
    291        """Greater-than operator, iplemented in terms of original Python 2 compare operator."""
    292        return self.__cmp__(other) > 0
    293
    294 -   def __cmp__(self, other):
    295        """
    296        Original Python 2 comparison operator.
    297        Lists within this class are "unordered" for equality comparisons.
    298        @param other: Other object to compare to.
    299        @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
    300        """
    301        if other is None:
    302           return 1
    303        if self.encrypt != other.encrypt:
    304           if self.encrypt < other.encrypt:
    305              return -1
    306           else:
    307              return 1
    308        return 0
    309
    310 -   def _setEncrypt(self, value):
    311        """
    312        Property target used to set the encrypt configuration value.
    313        If not C{None}, the value must be a C{EncryptConfig} object.
    314        @raise ValueError: If the value is not a C{EncryptConfig}
    315        """
    316        if value is None:
    317           self._encrypt = None
    318        else:
    319           if not isinstance(value, EncryptConfig):
    320              raise ValueError("Value must be a C{EncryptConfig} object.")
    321           self._encrypt = value
    322
    323 -   def _getEncrypt(self):
    324        """
    325        Property target used to get the encrypt configuration value.
    326        """
    327        return self._encrypt
    328
    329     encrypt = property(_getEncrypt, _setEncrypt, None, "Encrypt configuration in terms of a C{EncryptConfig} object.")
    330
    331 -   def validate(self):
    332        """
    333        Validates configuration represented by the object.
    334
    335        Encrypt configuration must be filled in.  Within that, both the encrypt
    336        mode and encrypt target must be filled in.
    337
    338        @raise ValueError: If one of the validations fails.
    339        """
    340        if self.encrypt is None:
    341           raise ValueError("Encrypt section is required.")
    342        if self.encrypt.encryptMode is None:
    343           raise ValueError("Encrypt mode must be set.")
    344        if self.encrypt.encryptTarget is None:
    345           raise ValueError("Encrypt target must be set.")
    346
    347 -   def addConfig(self, xmlDom, parentNode):
    348        """
    349        Adds an <encrypt> configuration section as the next child of a parent.
    350
    351        Third parties should use this function to write configuration related to
    352        this extension.
    353
    354        We add the following fields to the document::
    355
    356           encryptMode    //cb_config/encrypt/encrypt_mode
    357           encryptTarget  //cb_config/encrypt/encrypt_target
    358
    359        @param xmlDom: DOM tree as from C{impl.createDocument()}.
    360        @param parentNode: Parent that the section should be appended to.
    361        """
    362        if self.encrypt is not None:
    363           sectionNode = addContainerNode(xmlDom, parentNode, "encrypt")
    364           addStringNode(xmlDom, sectionNode, "encrypt_mode", self.encrypt.encryptMode)
    365           addStringNode(xmlDom, sectionNode, "encrypt_target", self.encrypt.encryptTarget)
    366
    367 -   def _parseXmlData(self, xmlData):
    368        """
    369        Internal method to parse an XML string into the object.
    370
    371        This method parses the XML document into a DOM tree (C{xmlDom}) and then
    372        calls a static method to parse the encrypt configuration section.
    373
    374        @param xmlData: XML data to be parsed
    375        @type xmlData: String data
    376
    377        @raise ValueError: If the XML cannot be successfully parsed.
    378        """
    379        (xmlDom, parentNode) = createInputDom(xmlData)
    380        self._encrypt = LocalConfig._parseEncrypt(parentNode)
    381
    382     @staticmethod
    383 -   def _parseEncrypt(parent):
    384        """
    385        Parses an encrypt configuration section.
    386
    387        We read the following individual fields::
    388
    389           encryptMode    //cb_config/encrypt/encrypt_mode
    390           encryptTarget  //cb_config/encrypt/encrypt_target
    391
    392        @param parent: Parent node to search beneath.
    393
    394        @return: C{EncryptConfig} object or C{None} if the section does not exist.
    395        @raise ValueError: If some filled-in value is invalid.
    396        """
    397        encrypt = None
    398        section = readFirstChild(parent, "encrypt")
    399        if section is not None:
    400           encrypt = EncryptConfig()
    401           encrypt.encryptMode = readString(section, "encrypt_mode")
    402           encrypt.encryptTarget = readString(section, "encrypt_target")
    403        return encrypt
    404
    405
    406  ########################################################################
    407  # Public functions
    408  ########################################################################
    409
    410  ###########################
    411  # executeAction() function
    412  ###########################
    413
    414 -def executeAction(configPath, options, config):
    415     """
    416     Executes the encrypt backup action.
    417
    418     @param configPath: Path to configuration file on disk.
    419     @type configPath: String representing a path on disk.
    420
    421     @param options: Program command-line options.
    422     @type options: Options object.
    423
    424     @param config: Program configuration.
    425     @type config: Config object.
    426
    427     @raise ValueError: Under many generic error conditions
    428     @raise IOError: If there are I/O problems reading or writing files
    429     """
    430     logger.debug("Executing encrypt extended action.")
    431     if config.options is None or config.stage is None:
    432        raise ValueError("Cedar Backup configuration is not properly filled in.")
    433     local = LocalConfig(xmlPath=configPath)
    434     if local.encrypt.encryptMode not in ["gpg", ]:
    435        raise ValueError("Unknown encrypt mode [%s]" % local.encrypt.encryptMode)
    436     if local.encrypt.encryptMode == "gpg":
    437        _confirmGpgRecipient(local.encrypt.encryptTarget)
    438     dailyDirs = findDailyDirs(config.stage.targetDir, ENCRYPT_INDICATOR)
    439     for dailyDir in dailyDirs:
    440        _encryptDailyDir(dailyDir, local.encrypt.encryptMode, local.encrypt.encryptTarget,
    441                         config.options.backupUser, config.options.backupGroup)
    442        writeIndicatorFile(dailyDir, ENCRYPT_INDICATOR, config.options.backupUser, config.options.backupGroup)
    443     logger.info("Executed the encrypt extended action successfully.")
    444
    445
    446  ##############################
    447  # _encryptDailyDir() function
    448  ##############################
    449
    450 -def _encryptDailyDir(dailyDir, encryptMode, encryptTarget, backupUser, backupGroup):
    451     """
    452     Encrypts the contents of a daily staging directory.
    453
    454     Indicator files are ignored.  All other files are encrypted.  The only valid
    455     encrypt mode is C{"gpg"}.
    456
    457     @param dailyDir: Daily directory to encrypt
    458     @param encryptMode: Encryption mode (only "gpg" is allowed)
    459     @param encryptTarget: Encryption target (GPG recipient for "gpg" mode)
    460     @param backupUser: User that target files should be owned by
    461     @param backupGroup: Group that target files should be owned by
    462
    463     @raise ValueError: If the encrypt mode is not supported.
    464     @raise ValueError: If the daily staging directory does not exist.
    465     """
    466     logger.debug("Begin encrypting contents of [%s].", dailyDir)
    467     fileList = getBackupFiles(dailyDir) # ignores indicator files
    468     for path in fileList:
    469        _encryptFile(path, encryptMode, encryptTarget, backupUser, backupGroup, removeSource=True)
    470     logger.debug("Completed encrypting contents of [%s].", dailyDir)
    471
    472
    473  ##########################
    474  # _encryptFile() function
    475  ##########################
    476
    477 -def _encryptFile(sourcePath, encryptMode, encryptTarget, backupUser, backupGroup, removeSource=False):
    478     """
    479     Encrypts the source file using the indicated mode.
    480
    481     The encrypted file will be owned by the indicated backup user and group.  If
    482     C{removeSource} is C{True}, then the source file will be removed after it is
    483     successfully encrypted.
    484
    485     Currently, only the C{"gpg"} encrypt mode is supported.
    486
    487     @param sourcePath: Absolute path of the source file to encrypt
    488     @param encryptMode: Encryption mode (only "gpg" is allowed)
    489     @param encryptTarget: Encryption target (GPG recipient)
    490     @param backupUser: User that target files should be owned by
    491     @param backupGroup: Group that target files should be owned by
    492     @param removeSource: Indicates whether to remove the source file
    493
    494     @return: Path to the newly-created encrypted file.
    495
    496     @raise ValueError: If an invalid encrypt mode is passed in.
    497     @raise IOError: If there is a problem accessing, encrypting or removing the source file.
    498     """
    499     if not os.path.exists(sourcePath):
    500        raise ValueError("Source path [%s] does not exist." % sourcePath)
    501     if encryptMode == 'gpg':
    502        encryptedPath = _encryptFileWithGpg(sourcePath, recipient=encryptTarget)
    503     else:
    504        raise ValueError("Unknown encrypt mode [%s]" % encryptMode)
    505     changeOwnership(encryptedPath, backupUser, backupGroup)
    506     if removeSource:
    507        if os.path.exists(sourcePath):
    508           try:
    509              os.remove(sourcePath)
    510              logger.debug("Completed removing old file [%s].", sourcePath)
    511           except:
    512              raise IOError("Failed to remove file [%s] after encrypting it." % (sourcePath))
    513     return encryptedPath
    514
    515
    516  #################################
    517  # _encryptFileWithGpg() function
    518  #################################
    519
    520 -def _encryptFileWithGpg(sourcePath, recipient):
    521     """
    522     Encrypts the indicated source file using GPG.
    523
    524     The encrypted file will be in GPG's binary output format and will have the
    525     same name as the source file plus a C{".gpg"} extension.  The source file
    526     will not be modified or removed by this function call.
    527
    528     @param sourcePath: Absolute path of file to be encrypted.
    529     @param recipient: Recipient name to be passed to GPG's C{"-r"} option
    530
    531     @return: Path to the newly-created encrypted file.
    532
    533     @raise IOError: If there is a problem encrypting the file.
    534     """
    535     encryptedPath = "%s.gpg" % sourcePath
    536     command = resolveCommand(GPG_COMMAND)
    537     args = [ "--batch", "--yes", "-e", "-r", recipient, "-o", encryptedPath, sourcePath, ]
    538     result = executeCommand(command, args)[0]
    539     if result != 0:
    540        raise IOError("Error [%d] calling gpg to encrypt [%s]." % (result, sourcePath))
    541     if not os.path.exists(encryptedPath):
    542        raise IOError("After call to [%s], encrypted file [%s] does not exist." % (command, encryptedPath))
    543     logger.debug("Completed encrypting file [%s] to [%s].", sourcePath, encryptedPath)
    544     return encryptedPath
    545
    546
    547  #################################
    548  # _confirmGpgRecpient() function
    549  #################################
    550
    551 -def _confirmGpgRecipient(recipient):
    552     """
    553     Confirms that a recipient's public key is known to GPG.
    554     Throws an exception if there is a problem, or returns normally otherwise.
    555     @param recipient: Recipient name
    556     @raise IOError: If the recipient's public key is not known to GPG.
    557     """
    558     command = resolveCommand(GPG_COMMAND)
    559     args = [ "--batch", "-k", recipient, ]  # should use --with-colons if the output will be parsed
    560     result = executeCommand(command, args)[0]
    561     if result != 0:
    562        raise IOError("GPG unable to find public key for [%s]." % recipient)
    563