1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
57
58
59
60 import os
61 import logging
62 from functools import total_ordering
63
64
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
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"
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
118 """
119 Official string representation for class instance.
120 """
121 return "EncryptConfig(%s, %s)" % (self.encryptMode, self.encryptTarget)
122
124 """
125 Informal string representation for class instance.
126 """
127 return self.__repr__()
128
130 """Equals operator, iplemented in terms of original Python 2 compare operator."""
131 return self.__cmp__(other) == 0
132
134 """Less-than operator, iplemented in terms of original Python 2 compare operator."""
135 return self.__cmp__(other) < 0
136
138 """Greater-than operator, iplemented in terms of original Python 2 compare operator."""
139 return self.__cmp__(other) > 0
140
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
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
174 """
175 Property target used to get the encrypt mode.
176 """
177 return self._encryptMode
178
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
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
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
271 """
272 Official string representation for class instance.
273 """
274 return "LocalConfig(%s)" % (self.encrypt)
275
277 """
278 Informal string representation for class instance.
279 """
280 return self.__repr__()
281
283 """Equals operator, iplemented in terms of original Python 2 compare operator."""
284 return self.__cmp__(other) == 0
285
287 """Less-than operator, iplemented in terms of original Python 2 compare operator."""
288 return self.__cmp__(other) < 0
289
291 """Greater-than operator, iplemented in terms of original Python 2 compare operator."""
292 return self.__cmp__(other) > 0
293
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
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
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
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
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
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
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
408
409
410
411
412
413
414
415 -def executeAction(configPath, options, config):
445
446
447
448
449
450
451 -def _encryptDailyDir(dailyDir, encryptMode, encryptTarget, backupUser, backupGroup):
452 """
453 Encrypts the contents of a daily staging directory.
454
455 Indicator files are ignored. All other files are encrypted. The only valid
456 encrypt mode is C{"gpg"}.
457
458 @param dailyDir: Daily directory to encrypt
459 @param encryptMode: Encryption mode (only "gpg" is allowed)
460 @param encryptTarget: Encryption target (GPG recipient for "gpg" mode)
461 @param backupUser: User that target files should be owned by
462 @param backupGroup: Group that target files should be owned by
463
464 @raise ValueError: If the encrypt mode is not supported.
465 @raise ValueError: If the daily staging directory does not exist.
466 """
467 logger.debug("Begin encrypting contents of [%s].", dailyDir)
468 fileList = getBackupFiles(dailyDir)
469 for path in fileList:
470 _encryptFile(path, encryptMode, encryptTarget, backupUser, backupGroup, removeSource=True)
471 logger.debug("Completed encrypting contents of [%s].", dailyDir)
472
473
474
475
476
477
478 -def _encryptFile(sourcePath, encryptMode, encryptTarget, backupUser, backupGroup, removeSource=False):
479 """
480 Encrypts the source file using the indicated mode.
481
482 The encrypted file will be owned by the indicated backup user and group. If
483 C{removeSource} is C{True}, then the source file will be removed after it is
484 successfully encrypted.
485
486 Currently, only the C{"gpg"} encrypt mode is supported.
487
488 @param sourcePath: Absolute path of the source file to encrypt
489 @param encryptMode: Encryption mode (only "gpg" is allowed)
490 @param encryptTarget: Encryption target (GPG recipient)
491 @param backupUser: User that target files should be owned by
492 @param backupGroup: Group that target files should be owned by
493 @param removeSource: Indicates whether to remove the source file
494
495 @return: Path to the newly-created encrypted file.
496
497 @raise ValueError: If an invalid encrypt mode is passed in.
498 @raise IOError: If there is a problem accessing, encrypting or removing the source file.
499 """
500 if not os.path.exists(sourcePath):
501 raise ValueError("Source path [%s] does not exist." % sourcePath)
502 if encryptMode == 'gpg':
503 encryptedPath = _encryptFileWithGpg(sourcePath, recipient=encryptTarget)
504 else:
505 raise ValueError("Unknown encrypt mode [%s]" % encryptMode)
506 changeOwnership(encryptedPath, backupUser, backupGroup)
507 if removeSource:
508 if os.path.exists(sourcePath):
509 try:
510 os.remove(sourcePath)
511 logger.debug("Completed removing old file [%s].", sourcePath)
512 except:
513 raise IOError("Failed to remove file [%s] after encrypting it." % (sourcePath))
514 return encryptedPath
515
522 """
523 Encrypts the indicated source file using GPG.
524
525 The encrypted file will be in GPG's binary output format and will have the
526 same name as the source file plus a C{".gpg"} extension. The source file
527 will not be modified or removed by this function call.
528
529 @param sourcePath: Absolute path of file to be encrypted.
530 @param recipient: Recipient name to be passed to GPG's C{"-r"} option
531
532 @return: Path to the newly-created encrypted file.
533
534 @raise IOError: If there is a problem encrypting the file.
535 """
536 encryptedPath = "%s.gpg" % sourcePath
537 command = resolveCommand(GPG_COMMAND)
538 args = [ "--batch", "--yes", "-e", "-r", recipient, "-o", encryptedPath, sourcePath, ]
539 result = executeCommand(command, args)[0]
540 if result != 0:
541 raise IOError("Error [%d] calling gpg to encrypt [%s]." % (result, sourcePath))
542 if not os.path.exists(encryptedPath):
543 raise IOError("After call to [%s], encrypted file [%s] does not exist." % (command, encryptedPath))
544 logger.debug("Completed encrypting file [%s] to [%s].", sourcePath, encryptedPath)
545 return encryptedPath
546
553 """
554 Confirms that a recipient's public key is known to GPG.
555 Throws an exception if there is a problem, or returns normally otherwise.
556 @param recipient: Recipient name
557 @raise IOError: If the recipient's public key is not known to GPG.
558 """
559 command = resolveCommand(GPG_COMMAND)
560 args = [ "--batch", "-k", recipient, ]
561 result = executeCommand(command, args)[0]
562 if result != 0:
563 raise IOError("GPG unable to find public key for [%s]." % recipient)
564