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
63
64 from CedarBackup2.util import resolveCommand, executeCommand, changeOwnership
65 from CedarBackup2.xmlutil import createInputDom, addContainerNode, addStringNode
66 from CedarBackup2.xmlutil import readFirstChild, readString
67 from CedarBackup2.actions.util import findDailyDirs, writeIndicatorFile, getBackupFiles
68
69
70
71
72
73
74 logger = logging.getLogger("CedarBackup2.log.extend.encrypt")
75
76 GPG_COMMAND = [ "gpg", ]
77 VALID_ENCRYPT_MODES = [ "gpg", ]
78 ENCRYPT_INDICATOR = "cback.encrypt"
86
87 """
88 Class representing encrypt configuration.
89
90 Encrypt configuration is used for encrypting staging directories.
91
92 The following restrictions exist on data in this class:
93
94 - The encrypt mode must be one of the values in L{VALID_ENCRYPT_MODES}
95 - The encrypt target value must be a non-empty string
96
97 @sort: __init__, __repr__, __str__, __cmp__, encryptMode, encryptTarget
98 """
99
100 - def __init__(self, encryptMode=None, encryptTarget=None):
101 """
102 Constructor for the C{EncryptConfig} class.
103
104 @param encryptMode: Encryption mode
105 @param encryptTarget: Encryption target (for instance, GPG recipient)
106
107 @raise ValueError: If one of the values is invalid.
108 """
109 self._encryptMode = None
110 self._encryptTarget = None
111 self.encryptMode = encryptMode
112 self.encryptTarget = encryptTarget
113
115 """
116 Official string representation for class instance.
117 """
118 return "EncryptConfig(%s, %s)" % (self.encryptMode, self.encryptTarget)
119
121 """
122 Informal string representation for class instance.
123 """
124 return self.__repr__()
125
127 """
128 Definition of equals operator for this class.
129 Lists within this class are "unordered" for equality comparisons.
130 @param other: Other object to compare to.
131 @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
132 """
133 if other is None:
134 return 1
135 if self.encryptMode != other.encryptMode:
136 if self.encryptMode < other.encryptMode:
137 return -1
138 else:
139 return 1
140 if self.encryptTarget != other.encryptTarget:
141 if self.encryptTarget < other.encryptTarget:
142 return -1
143 else:
144 return 1
145 return 0
146
148 """
149 Property target used to set the encrypt mode.
150 If not C{None}, the mode must be one of the values in L{VALID_ENCRYPT_MODES}.
151 @raise ValueError: If the value is not valid.
152 """
153 if value is not None:
154 if value not in VALID_ENCRYPT_MODES:
155 raise ValueError("Encrypt mode must be one of %s." % VALID_ENCRYPT_MODES)
156 self._encryptMode = value
157
159 """
160 Property target used to get the encrypt mode.
161 """
162 return self._encryptMode
163
165 """
166 Property target used to set the encrypt target.
167 """
168 if value is not None:
169 if len(value) < 1:
170 raise ValueError("Encrypt target must be non-empty string.")
171 self._encryptTarget = value
172
174 """
175 Property target used to get the encrypt target.
176 """
177 return self._encryptTarget
178
179 encryptMode = property(_getEncryptMode, _setEncryptMode, None, doc="Encrypt mode.")
180 encryptTarget = property(_getEncryptTarget, _setEncryptTarget, None, doc="Encrypt target (i.e. GPG recipient).")
181
188
189 """
190 Class representing this extension's configuration document.
191
192 This is not a general-purpose configuration object like the main Cedar
193 Backup configuration object. Instead, it just knows how to parse and emit
194 encrypt-specific configuration values. Third parties who need to read and
195 write configuration related to this extension should access it through the
196 constructor, C{validate} and C{addConfig} methods.
197
198 @note: Lists within this class are "unordered" for equality comparisons.
199
200 @sort: __init__, __repr__, __str__, __cmp__, encrypt, validate, addConfig
201 """
202
203 - def __init__(self, xmlData=None, xmlPath=None, validate=True):
204 """
205 Initializes a configuration object.
206
207 If you initialize the object without passing either C{xmlData} or
208 C{xmlPath} then configuration will be empty and will be invalid until it
209 is filled in properly.
210
211 No reference to the original XML data or original path is saved off by
212 this class. Once the data has been parsed (successfully or not) this
213 original information is discarded.
214
215 Unless the C{validate} argument is C{False}, the L{LocalConfig.validate}
216 method will be called (with its default arguments) against configuration
217 after successfully parsing any passed-in XML. Keep in mind that even if
218 C{validate} is C{False}, it might not be possible to parse the passed-in
219 XML document if lower-level validations fail.
220
221 @note: It is strongly suggested that the C{validate} option always be set
222 to C{True} (the default) unless there is a specific need to read in
223 invalid configuration from disk.
224
225 @param xmlData: XML data representing configuration.
226 @type xmlData: String data.
227
228 @param xmlPath: Path to an XML file on disk.
229 @type xmlPath: Absolute path to a file on disk.
230
231 @param validate: Validate the document after parsing it.
232 @type validate: Boolean true/false.
233
234 @raise ValueError: If both C{xmlData} and C{xmlPath} are passed-in.
235 @raise ValueError: If the XML data in C{xmlData} or C{xmlPath} cannot be parsed.
236 @raise ValueError: If the parsed configuration document is not valid.
237 """
238 self._encrypt = None
239 self.encrypt = None
240 if xmlData is not None and xmlPath is not None:
241 raise ValueError("Use either xmlData or xmlPath, but not both.")
242 if xmlData is not None:
243 self._parseXmlData(xmlData)
244 if validate:
245 self.validate()
246 elif xmlPath is not None:
247 xmlData = open(xmlPath).read()
248 self._parseXmlData(xmlData)
249 if validate:
250 self.validate()
251
253 """
254 Official string representation for class instance.
255 """
256 return "LocalConfig(%s)" % (self.encrypt)
257
259 """
260 Informal string representation for class instance.
261 """
262 return self.__repr__()
263
265 """
266 Definition of equals operator for this class.
267 Lists within this class are "unordered" for equality comparisons.
268 @param other: Other object to compare to.
269 @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
270 """
271 if other is None:
272 return 1
273 if self.encrypt != other.encrypt:
274 if self.encrypt < other.encrypt:
275 return -1
276 else:
277 return 1
278 return 0
279
281 """
282 Property target used to set the encrypt configuration value.
283 If not C{None}, the value must be a C{EncryptConfig} object.
284 @raise ValueError: If the value is not a C{EncryptConfig}
285 """
286 if value is None:
287 self._encrypt = None
288 else:
289 if not isinstance(value, EncryptConfig):
290 raise ValueError("Value must be a C{EncryptConfig} object.")
291 self._encrypt = value
292
294 """
295 Property target used to get the encrypt configuration value.
296 """
297 return self._encrypt
298
299 encrypt = property(_getEncrypt, _setEncrypt, None, "Encrypt configuration in terms of a C{EncryptConfig} object.")
300
302 """
303 Validates configuration represented by the object.
304
305 Encrypt configuration must be filled in. Within that, both the encrypt
306 mode and encrypt target must be filled in.
307
308 @raise ValueError: If one of the validations fails.
309 """
310 if self.encrypt is None:
311 raise ValueError("Encrypt section is required.")
312 if self.encrypt.encryptMode is None:
313 raise ValueError("Encrypt mode must be set.")
314 if self.encrypt.encryptTarget is None:
315 raise ValueError("Encrypt target must be set.")
316
318 """
319 Adds an <encrypt> configuration section as the next child of a parent.
320
321 Third parties should use this function to write configuration related to
322 this extension.
323
324 We add the following fields to the document::
325
326 encryptMode //cb_config/encrypt/encrypt_mode
327 encryptTarget //cb_config/encrypt/encrypt_target
328
329 @param xmlDom: DOM tree as from C{impl.createDocument()}.
330 @param parentNode: Parent that the section should be appended to.
331 """
332 if self.encrypt is not None:
333 sectionNode = addContainerNode(xmlDom, parentNode, "encrypt")
334 addStringNode(xmlDom, sectionNode, "encrypt_mode", self.encrypt.encryptMode)
335 addStringNode(xmlDom, sectionNode, "encrypt_target", self.encrypt.encryptTarget)
336
338 """
339 Internal method to parse an XML string into the object.
340
341 This method parses the XML document into a DOM tree (C{xmlDom}) and then
342 calls a static method to parse the encrypt configuration section.
343
344 @param xmlData: XML data to be parsed
345 @type xmlData: String data
346
347 @raise ValueError: If the XML cannot be successfully parsed.
348 """
349 (xmlDom, parentNode) = createInputDom(xmlData)
350 self._encrypt = LocalConfig._parseEncrypt(parentNode)
351
352 @staticmethod
354 """
355 Parses an encrypt configuration section.
356
357 We read the following individual fields::
358
359 encryptMode //cb_config/encrypt/encrypt_mode
360 encryptTarget //cb_config/encrypt/encrypt_target
361
362 @param parent: Parent node to search beneath.
363
364 @return: C{EncryptConfig} object or C{None} if the section does not exist.
365 @raise ValueError: If some filled-in value is invalid.
366 """
367 encrypt = None
368 section = readFirstChild(parent, "encrypt")
369 if section is not None:
370 encrypt = EncryptConfig()
371 encrypt.encryptMode = readString(section, "encrypt_mode")
372 encrypt.encryptTarget = readString(section, "encrypt_target")
373 return encrypt
374
375
376
377
378
379
380
381
382
383
384
385 -def executeAction(configPath, options, config):
415
416
417
418
419
420
421 -def _encryptDailyDir(dailyDir, encryptMode, encryptTarget, backupUser, backupGroup):
422 """
423 Encrypts the contents of a daily staging directory.
424
425 Indicator files are ignored. All other files are encrypted. The only valid
426 encrypt mode is C{"gpg"}.
427
428 @param dailyDir: Daily directory to encrypt
429 @param encryptMode: Encryption mode (only "gpg" is allowed)
430 @param encryptTarget: Encryption target (GPG recipient for "gpg" mode)
431 @param backupUser: User that target files should be owned by
432 @param backupGroup: Group that target files should be owned by
433
434 @raise ValueError: If the encrypt mode is not supported.
435 @raise ValueError: If the daily staging directory does not exist.
436 """
437 logger.debug("Begin encrypting contents of [%s].", dailyDir)
438 fileList = getBackupFiles(dailyDir)
439 for path in fileList:
440 _encryptFile(path, encryptMode, encryptTarget, backupUser, backupGroup, removeSource=True)
441 logger.debug("Completed encrypting contents of [%s].", dailyDir)
442
443
444
445
446
447
448 -def _encryptFile(sourcePath, encryptMode, encryptTarget, backupUser, backupGroup, removeSource=False):
449 """
450 Encrypts the source file using the indicated mode.
451
452 The encrypted file will be owned by the indicated backup user and group. If
453 C{removeSource} is C{True}, then the source file will be removed after it is
454 successfully encrypted.
455
456 Currently, only the C{"gpg"} encrypt mode is supported.
457
458 @param sourcePath: Absolute path of the source file to encrypt
459 @param encryptMode: Encryption mode (only "gpg" is allowed)
460 @param encryptTarget: Encryption target (GPG recipient)
461 @param backupUser: User that target files should be owned by
462 @param backupGroup: Group that target files should be owned by
463 @param removeSource: Indicates whether to remove the source file
464
465 @return: Path to the newly-created encrypted file.
466
467 @raise ValueError: If an invalid encrypt mode is passed in.
468 @raise IOError: If there is a problem accessing, encrypting or removing the source file.
469 """
470 if not os.path.exists(sourcePath):
471 raise ValueError("Source path [%s] does not exist." % sourcePath)
472 if encryptMode == 'gpg':
473 encryptedPath = _encryptFileWithGpg(sourcePath, recipient=encryptTarget)
474 else:
475 raise ValueError("Unknown encrypt mode [%s]" % encryptMode)
476 changeOwnership(encryptedPath, backupUser, backupGroup)
477 if removeSource:
478 if os.path.exists(sourcePath):
479 try:
480 os.remove(sourcePath)
481 logger.debug("Completed removing old file [%s].", sourcePath)
482 except:
483 raise IOError("Failed to remove file [%s] after encrypting it." % (sourcePath))
484 return encryptedPath
485
492 """
493 Encrypts the indicated source file using GPG.
494
495 The encrypted file will be in GPG's binary output format and will have the
496 same name as the source file plus a C{".gpg"} extension. The source file
497 will not be modified or removed by this function call.
498
499 @param sourcePath: Absolute path of file to be encrypted.
500 @param recipient: Recipient name to be passed to GPG's C{"-r"} option
501
502 @return: Path to the newly-created encrypted file.
503
504 @raise IOError: If there is a problem encrypting the file.
505 """
506 encryptedPath = "%s.gpg" % sourcePath
507 command = resolveCommand(GPG_COMMAND)
508 args = [ "--batch", "--yes", "-e", "-r", recipient, "-o", encryptedPath, sourcePath, ]
509 result = executeCommand(command, args)[0]
510 if result != 0:
511 raise IOError("Error [%d] calling gpg to encrypt [%s]." % (result, sourcePath))
512 if not os.path.exists(encryptedPath):
513 raise IOError("After call to [%s], encrypted file [%s] does not exist." % (command, encryptedPath))
514 logger.debug("Completed encrypting file [%s] to [%s].", sourcePath, encryptedPath)
515 return encryptedPath
516
523 """
524 Confirms that a recipient's public key is known to GPG.
525 Throws an exception if there is a problem, or returns normally otherwise.
526 @param recipient: Recipient name
527 @raise IOError: If the recipient's public key is not known to GPG.
528 """
529 command = resolveCommand(GPG_COMMAND)
530 args = [ "--batch", "-k", recipient, ]
531 result = executeCommand(command, args)[0]
532 if result != 0:
533 raise IOError("GPG unable to find public key for [%s]." % recipient)
534