cm-backup 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +20 -0
  3. data/bin/backup +5 -0
  4. data/lib/backup.rb +144 -0
  5. data/lib/backup/archive.rb +170 -0
  6. data/lib/backup/binder.rb +22 -0
  7. data/lib/backup/cleaner.rb +116 -0
  8. data/lib/backup/cli.rb +374 -0
  9. data/lib/backup/cloud_io/base.rb +41 -0
  10. data/lib/backup/cloud_io/cloud_files.rb +298 -0
  11. data/lib/backup/cloud_io/s3.rb +260 -0
  12. data/lib/backup/compressor/base.rb +35 -0
  13. data/lib/backup/compressor/bzip2.rb +39 -0
  14. data/lib/backup/compressor/custom.rb +53 -0
  15. data/lib/backup/compressor/gzip.rb +74 -0
  16. data/lib/backup/config.rb +119 -0
  17. data/lib/backup/config/dsl.rb +103 -0
  18. data/lib/backup/config/helpers.rb +143 -0
  19. data/lib/backup/database/base.rb +85 -0
  20. data/lib/backup/database/mongodb.rb +187 -0
  21. data/lib/backup/database/mysql.rb +192 -0
  22. data/lib/backup/database/openldap.rb +95 -0
  23. data/lib/backup/database/postgresql.rb +133 -0
  24. data/lib/backup/database/redis.rb +179 -0
  25. data/lib/backup/database/riak.rb +82 -0
  26. data/lib/backup/database/sqlite.rb +57 -0
  27. data/lib/backup/encryptor/base.rb +29 -0
  28. data/lib/backup/encryptor/gpg.rb +747 -0
  29. data/lib/backup/encryptor/open_ssl.rb +77 -0
  30. data/lib/backup/errors.rb +58 -0
  31. data/lib/backup/logger.rb +199 -0
  32. data/lib/backup/logger/console.rb +51 -0
  33. data/lib/backup/logger/fog_adapter.rb +29 -0
  34. data/lib/backup/logger/logfile.rb +133 -0
  35. data/lib/backup/logger/syslog.rb +116 -0
  36. data/lib/backup/model.rb +479 -0
  37. data/lib/backup/notifier/base.rb +128 -0
  38. data/lib/backup/notifier/campfire.rb +63 -0
  39. data/lib/backup/notifier/command.rb +102 -0
  40. data/lib/backup/notifier/datadog.rb +107 -0
  41. data/lib/backup/notifier/flowdock.rb +103 -0
  42. data/lib/backup/notifier/hipchat.rb +118 -0
  43. data/lib/backup/notifier/http_post.rb +117 -0
  44. data/lib/backup/notifier/mail.rb +249 -0
  45. data/lib/backup/notifier/nagios.rb +69 -0
  46. data/lib/backup/notifier/pagerduty.rb +81 -0
  47. data/lib/backup/notifier/prowl.rb +68 -0
  48. data/lib/backup/notifier/pushover.rb +74 -0
  49. data/lib/backup/notifier/ses.rb +105 -0
  50. data/lib/backup/notifier/slack.rb +148 -0
  51. data/lib/backup/notifier/twitter.rb +58 -0
  52. data/lib/backup/notifier/zabbix.rb +63 -0
  53. data/lib/backup/package.rb +55 -0
  54. data/lib/backup/packager.rb +107 -0
  55. data/lib/backup/pipeline.rb +124 -0
  56. data/lib/backup/splitter.rb +76 -0
  57. data/lib/backup/storage/base.rb +69 -0
  58. data/lib/backup/storage/cloud_files.rb +158 -0
  59. data/lib/backup/storage/cycler.rb +75 -0
  60. data/lib/backup/storage/dropbox.rb +212 -0
  61. data/lib/backup/storage/ftp.rb +112 -0
  62. data/lib/backup/storage/local.rb +64 -0
  63. data/lib/backup/storage/qiniu.rb +65 -0
  64. data/lib/backup/storage/rsync.rb +248 -0
  65. data/lib/backup/storage/s3.rb +156 -0
  66. data/lib/backup/storage/scp.rb +67 -0
  67. data/lib/backup/storage/sftp.rb +82 -0
  68. data/lib/backup/syncer/base.rb +70 -0
  69. data/lib/backup/syncer/cloud/base.rb +179 -0
  70. data/lib/backup/syncer/cloud/cloud_files.rb +83 -0
  71. data/lib/backup/syncer/cloud/local_file.rb +100 -0
  72. data/lib/backup/syncer/cloud/s3.rb +110 -0
  73. data/lib/backup/syncer/rsync/base.rb +54 -0
  74. data/lib/backup/syncer/rsync/local.rb +31 -0
  75. data/lib/backup/syncer/rsync/pull.rb +51 -0
  76. data/lib/backup/syncer/rsync/push.rb +205 -0
  77. data/lib/backup/template.rb +46 -0
  78. data/lib/backup/utilities.rb +224 -0
  79. data/lib/backup/version.rb +5 -0
  80. data/templates/cli/archive +28 -0
  81. data/templates/cli/compressor/bzip2 +4 -0
  82. data/templates/cli/compressor/custom +7 -0
  83. data/templates/cli/compressor/gzip +4 -0
  84. data/templates/cli/config +123 -0
  85. data/templates/cli/databases/mongodb +15 -0
  86. data/templates/cli/databases/mysql +18 -0
  87. data/templates/cli/databases/openldap +24 -0
  88. data/templates/cli/databases/postgresql +16 -0
  89. data/templates/cli/databases/redis +16 -0
  90. data/templates/cli/databases/riak +17 -0
  91. data/templates/cli/databases/sqlite +11 -0
  92. data/templates/cli/encryptor/gpg +27 -0
  93. data/templates/cli/encryptor/openssl +9 -0
  94. data/templates/cli/model +26 -0
  95. data/templates/cli/notifier/zabbix +15 -0
  96. data/templates/cli/notifiers/campfire +12 -0
  97. data/templates/cli/notifiers/command +32 -0
  98. data/templates/cli/notifiers/datadog +57 -0
  99. data/templates/cli/notifiers/flowdock +16 -0
  100. data/templates/cli/notifiers/hipchat +16 -0
  101. data/templates/cli/notifiers/http_post +32 -0
  102. data/templates/cli/notifiers/mail +24 -0
  103. data/templates/cli/notifiers/nagios +13 -0
  104. data/templates/cli/notifiers/pagerduty +12 -0
  105. data/templates/cli/notifiers/prowl +11 -0
  106. data/templates/cli/notifiers/pushover +11 -0
  107. data/templates/cli/notifiers/ses +15 -0
  108. data/templates/cli/notifiers/slack +22 -0
  109. data/templates/cli/notifiers/twitter +13 -0
  110. data/templates/cli/splitter +7 -0
  111. data/templates/cli/storages/cloud_files +11 -0
  112. data/templates/cli/storages/dropbox +20 -0
  113. data/templates/cli/storages/ftp +13 -0
  114. data/templates/cli/storages/local +8 -0
  115. data/templates/cli/storages/qiniu +12 -0
  116. data/templates/cli/storages/rsync +17 -0
  117. data/templates/cli/storages/s3 +16 -0
  118. data/templates/cli/storages/scp +15 -0
  119. data/templates/cli/storages/sftp +15 -0
  120. data/templates/cli/syncers/cloud_files +22 -0
  121. data/templates/cli/syncers/rsync_local +20 -0
  122. data/templates/cli/syncers/rsync_pull +28 -0
  123. data/templates/cli/syncers/rsync_push +28 -0
  124. data/templates/cli/syncers/s3 +27 -0
  125. data/templates/general/links +3 -0
  126. data/templates/general/version.erb +2 -0
  127. data/templates/notifier/mail/failure.erb +16 -0
  128. data/templates/notifier/mail/success.erb +16 -0
  129. data/templates/notifier/mail/warning.erb +16 -0
  130. data/templates/storage/dropbox/authorization_url.erb +6 -0
  131. data/templates/storage/dropbox/authorized.erb +4 -0
  132. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  133. metadata +1077 -0
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ module Backup
4
+ module Encryptor
5
+ class Base
6
+ include Utilities::Helpers
7
+ include Config::Helpers
8
+
9
+ def initialize
10
+ load_defaults!
11
+ end
12
+
13
+ private
14
+
15
+ ##
16
+ # Return the encryptor name, with Backup namespace removed
17
+ def encryptor_name
18
+ self.class.to_s.sub('Backup::', '')
19
+ end
20
+
21
+ ##
22
+ # Logs a message to the console and log file to inform
23
+ # the client that Backup is encrypting the archive
24
+ def log!
25
+ Logger.info "Using #{ encryptor_name } to encrypt the archive."
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,747 @@
1
+ # encoding: utf-8
2
+
3
+ module Backup
4
+ module Encryptor
5
+ ##
6
+ # The GPG Encryptor allows you to encrypt your final archive using GnuPG,
7
+ # using one of three {#mode modes} of operation.
8
+ #
9
+ # == First, setup defaults in your +config.rb+ file
10
+ #
11
+ # Configure the {#keys} Hash using {.defaults} in your +config.rb+
12
+ # to specify all valid {#recipients} and their Public Key.
13
+ #
14
+ # Backup::Encryptor::GPG.defaults do |encryptor|
15
+ # # setup all GnuPG public keys
16
+ # encryptor.keys = {}
17
+ # encryptor.keys['joe@example.com'] = <<-EOS
18
+ # # ...public key here...
19
+ # EOS
20
+ # encryptor.keys['mary@example.com'] = <<-EOS
21
+ # # ...public key here...
22
+ # EOS
23
+ # end
24
+ #
25
+ # The optional {#gpg_config} and {#gpg_homedir} options would also
26
+ # typically be set using {.defaults} in +config.rb+ as well.
27
+ #
28
+ # == Then, setup each of your Models
29
+ #
30
+ # Set the desired {#recipients} and/or {#passphrase} (or {#passphrase_file})
31
+ # for each {Model}, depending on the {#mode} used.
32
+ #
33
+ # === my_backup_01
34
+ #
35
+ # This archive can only be decrypted using the private key for joe@example.com
36
+ #
37
+ # Model.new(:my_backup_01, 'Backup Job #1') do
38
+ # # ... archives, databases, compressor and storage options, etc...
39
+ # encrypt_with GPG do |encryptor|
40
+ # encryptor.mode = :asymmetric
41
+ # encryptor.recipients = 'joe@example.com'
42
+ # end
43
+ # end
44
+ #
45
+ # === my_backup_02
46
+ #
47
+ # This archive can only be decrypted using the passphrase "a secret".
48
+ #
49
+ # Model.new(:my_backup_02, 'Backup Job #2') do
50
+ # # ... archives, databases, compressor and storage options, etc...
51
+ # encrypt_with GPG do |encryptor|
52
+ # encryptor.mode = :symmetric
53
+ # encryptor.passphrase = 'a secret'
54
+ # end
55
+ # end
56
+ #
57
+ # === my_backup_03
58
+ #
59
+ # This archive may be decrypted using either the private key for joe@example.com
60
+ # *or* mary@example.com, *and* may also be decrypted using the passphrase.
61
+ #
62
+ # Model.new(:my_backup_03, 'Backup Job #3') do
63
+ # # ... archives, databases, compressor and storage options, etc...
64
+ # encrypt_with GPG do |encryptor|
65
+ # encryptor.mode = :both
66
+ # encryptor.passphrase = 'a secret'
67
+ # encryptor.recipients = ['joe@example.com', 'mary@example.com']
68
+ # end
69
+ # end
70
+ #
71
+ class GPG < Base
72
+ class Error < Backup::Error; end
73
+
74
+ MODES = [:asymmetric, :symmetric, :both]
75
+
76
+ ##
77
+ # Sets the mode of operation.
78
+ #
79
+ # [:asymmetric]
80
+ # In this mode, the final backup archive will be encrypted using the
81
+ # public key(s) specified by the key identifiers in {#recipients}.
82
+ # The archive may then be decrypted by anyone with a private key that
83
+ # corresponds to one of the public keys used. See {#recipients} and
84
+ # {#keys} for more information.
85
+ #
86
+ # [:symmetric]
87
+ # In this mode, the final backup archive will be encrypted using the
88
+ # passphrase specified by {#passphrase} or {#passphrase_file}.
89
+ # The archive will be encrypted using the encryption algorithm
90
+ # specified in your GnuPG configuration. See {#gpg_config} for more
91
+ # information. Anyone with the passphrase may decrypt the archive.
92
+ #
93
+ # [:both]
94
+ # In this mode, both +:asymmetric+ and +:symmetric+ options are used.
95
+ # Meaning that the archive may be decrypted by anyone with a valid
96
+ # private key or by using the proper passphrase.
97
+ #
98
+ # @param mode [String, Symbol] Sets the mode of operation.
99
+ # (Defaults to +:asymmetric+)
100
+ # @return [Symbol] mode that was set.
101
+ # @raise [Backup::Errors::Encryptor::GPG::InvalidModeError]
102
+ # if mode given is invalid.
103
+ #
104
+ attr_reader :mode
105
+ def mode=(mode)
106
+ @mode = mode.to_sym
107
+ raise Error, "'#{ @mode }' is not a valid mode." unless MODES.include?(@mode)
108
+ end
109
+
110
+ ##
111
+ # Specifies the GnuPG configuration to be used.
112
+ #
113
+ # This should be given as the text of a +gpg.conf+ file. It will be
114
+ # written to a temporary file, which will be passed to the +gpg+ command
115
+ # to use instead of the +gpg.conf+ found in the GnuPG home directory.
116
+ # This allows you to be certain your preferences are used.
117
+ #
118
+ # This is especially useful if you've also set {#gpg_homedir} and plan
119
+ # on allowing Backup to automatically create that directory and import
120
+ # all your public keys specified in {#keys}. In this situation, that
121
+ # folder would not contain any +gpg.conf+ file, so GnuPG would simply
122
+ # use it's defaults.
123
+ #
124
+ # While this may be specified on a per-Model basis, you would generally
125
+ # just specify this in the defaults. Leading tabs/spaces are stripped
126
+ # before writing the given string to the temporary configuration file.
127
+ #
128
+ # Backup::Encryptor::GPG.defaults do |enc|
129
+ # enc.gpg_config = <<-EOF
130
+ # # safely override preferences set in the receiver's public key(s)
131
+ # personal-cipher-preferences TWOFISH AES256 BLOWFISH AES192 CAST5 AES
132
+ # personal-digest-preferences SHA512 SHA256 SHA1 MD5
133
+ # personal-compress-preferences BZIP2 ZLIB ZIP Uncompressed
134
+ # # cipher algorithm for symmetric encryption
135
+ # # (if personal-cipher-preferences are not specified)
136
+ # s2k-cipher-algo TWOFISH
137
+ # # digest algorithm for mangling the symmetric encryption passphrase
138
+ # s2k-digest-algo SHA512
139
+ # EOF
140
+ # end
141
+ #
142
+ # @see #gpg_homedir
143
+ # @return [String]
144
+ attr_accessor :gpg_config
145
+
146
+ ##
147
+ # Set the GnuPG home directory to be used.
148
+ #
149
+ # This allows you to specify the GnuPG home directory on the system
150
+ # where Backup will be run, keeping the keyrings used by Backup separate
151
+ # from the default keyrings of the user running Backup.
152
+ # By default, this would be +`~/.gnupg`+.
153
+ #
154
+ # If a directory is specified here, Backup will create it if needed
155
+ # and ensure the correct permissions are set. All public keys Backup
156
+ # imports would be added to the +pubring.gpg+ file within this directory,
157
+ # and +gpg+ would be given this directory using it's +--homedir+ option.
158
+ #
159
+ # Any +gpg.conf+ file located in this directory would also be used by
160
+ # +gpg+, unless {#gpg_config} is specified.
161
+ #
162
+ # The given path will be expanded before use.
163
+ #
164
+ # @return [String]
165
+ attr_accessor :gpg_homedir
166
+
167
+ ##
168
+ # Specifies a Hash of public key identifiers and their public keys.
169
+ #
170
+ # While not _required_, it is recommended that all public keys you intend
171
+ # to use be setup in {#keys}. The best place to do this is in your defaults
172
+ # in +config.rb+.
173
+ #
174
+ # Backup::Encryptor::GPG.defaults do |enc|
175
+ # enc.keys = {}
176
+ #
177
+ # enc.keys['joe@example.com'] = <<-EOS
178
+ # -----BEGIN PGP PUBLIC KEY BLOCK-----
179
+ # Version: GnuPG v1.4.12 (GNU/Linux)
180
+ #
181
+ # mQMqBEd5F8MRCACfArHCJFR6nkmxNiW+UE4PAW3bQla9JWFqCwu4VqLkPI/lHb5p
182
+ # xHff8Fzy2O89BxD/6hXSDx2SlVmAGHOCJhShx1vfNGVYNsJn2oNK50in9kGvD0+m
183
+ # [...]
184
+ # SkQEHOxhMiFjAN9q4LuirSOu65uR1bnTmF+Z92++qMIuEkH4/LnN
185
+ # =8gNa
186
+ # -----END PGP PUBLIC KEY BLOCK-----
187
+ # EOS
188
+ #
189
+ # enc.keys['mary@example.com'] = <<-EOS
190
+ # -----BEGIN PGP PUBLIC KEY BLOCK-----
191
+ # Version: GnuPG v1.4.12 (GNU/Linux)
192
+ #
193
+ # 2SlVmAGHOCJhShx1vfNGVYNxHff8Fzy2O89BxD/6in9kGvD0+mhXSDxsJn2oNK50
194
+ # kmxNiW+UmQMqBEd5F8MRCACfArHCJFR6qCwu4VqLkPI/lHb5pnE4PAW3bQla9JWF
195
+ # [...]
196
+ # AN9q4LSkQEHOxhMiFjuirSOu65u++qMIuEkH4/LnNR1bnTmF+Z92
197
+ # =8gNa
198
+ # -----END PGP PUBLIC KEY BLOCK-----
199
+ #
200
+ # EOS
201
+ # end
202
+ #
203
+ # All leading spaces/tabs will be stripped from the key, so the above
204
+ # form may be used to set each identifier's key.
205
+ #
206
+ # When a public key can not be found for an identifier specified in
207
+ # {#recipients}, the corresponding public key from this Hash will be
208
+ # imported into +pubring.gpg+ in the GnuPG home directory ({#gpg_homedir}).
209
+ # Therefore, each key *must* be the same identifier used in {#recipients}.
210
+ #
211
+ # To obtain the public key in ASCII format, use:
212
+ #
213
+ # $ gpg -a --export joe@example.com
214
+ #
215
+ # See {#recipients} for information on what may be used as valid identifiers.
216
+ #
217
+ # @return [Hash]
218
+ attr_accessor :keys
219
+
220
+ ##
221
+ # Specifies the recipients to use when encrypting the backup archive.
222
+ #
223
+ # When {#mode} is set to +:asymmetric+ or +:both+, the public key for
224
+ # each recipient given here will be used to encrypt the archive. Each
225
+ # recipient will be able to decrypt the archive using their private key.
226
+ #
227
+ # If there is only one recipient, this may be specified as a String.
228
+ # Otherwise, this should be an Array of Strings. Each String must be a
229
+ # valid public key identifier, and *must* be the same identifier used to
230
+ # specify the recipient's public key in {#keys}. This is so that if a
231
+ # public key is not found for the given identifier, it may be imported
232
+ # from {#keys}.
233
+ #
234
+ # Valid identifiers which may be used are as follows:
235
+ #
236
+ # [Key Fingerprint]
237
+ # The key fingerprint is a 40-character hex string, which uniquely
238
+ # identifies a public key. This may be obtained using the following:
239
+ #
240
+ # $ gpg --fingerprint john.smith@example.com
241
+ # pub 1024R/4E5E8D8A 2012-07-20
242
+ # Key fingerprint = FFEA D1DB 201F B214 873E 7399 4A83 569F 4E5E 8D8A
243
+ # uid John Smith <john.smith@example.com>
244
+ # sub 1024R/92C8DFD8 2012-07-20
245
+ #
246
+ # [Long Key ID]
247
+ # The long Key ID is the last 16-characters of the key's fingerprint.
248
+ #
249
+ # The Long Key ID in this example is: 4A83569F4E5E8D8A
250
+ #
251
+ # $ gpg --keyid-format long -k john.smith@example.com
252
+ # pub 1024R/4A83569F4E5E8D8A 2012-07-20
253
+ # uid John Smith <john.smith@example.com>
254
+ # sub 1024R/662F18DB92C8DFD8 2012-07-20
255
+ #
256
+ # [Short Key ID]
257
+ # The short Key ID is the last 8-characters of the key's fingerprint.
258
+ # This is the default key format seen when listing keys.
259
+ #
260
+ # The Short Key ID in this example is: 4E5E8D8A
261
+ #
262
+ # $ gpg -k john.smith@example.com
263
+ # pub 1024R/4E5E8D8A 2012-07-20
264
+ # uid John Smith <john.smith@example.com>
265
+ # sub 1024R/92C8DFD8 2012-07-20
266
+ #
267
+ # [Email Address]
268
+ # This must exactly match an email address for one of the UID records
269
+ # associated with the recipient's public key.
270
+ #
271
+ # Recipient identifier forms may be mixed, as long as the identifier used
272
+ # here is the same as that used in {#keys}. Also, all spaces will be stripped
273
+ # from the identifier when used, so the following would be valid.
274
+ #
275
+ # Backup::Model.new(:my_backup, 'My Backup') do
276
+ # encrypt_with GPG do |enc|
277
+ # enc.recipients = [
278
+ # # John Smith
279
+ # '4A83 569F 4E5E 8D8A',
280
+ # # Mary Smith
281
+ # 'mary.smith@example.com'
282
+ # ]
283
+ # end
284
+ # end
285
+ #
286
+ # @return [String, Array]
287
+ attr_accessor :recipients
288
+
289
+ ##
290
+ # Specifies the passphrase to use symmetric encryption.
291
+ #
292
+ # When {#mode} is +:symmetric+ or +:both+, this passphrase will be used
293
+ # to symmetrically encrypt the archive.
294
+ #
295
+ # Use of this option will override the use of {#passphrase_file}.
296
+ #
297
+ # @return [String]
298
+ attr_accessor :passphrase
299
+
300
+ ##
301
+ # Specifies the passphrase file to use symmetric encryption.
302
+ #
303
+ # When {#mode} is +:symmetric+ or +:both+, this file will be passed
304
+ # to the +gpg+ command line, where +gpg+ will read the first line from
305
+ # this file and use it for the passphrase.
306
+ #
307
+ # The file path given here will be expanded to a full path.
308
+ #
309
+ # If {#passphrase} is specified, {#passphrase_file} will be ignored.
310
+ # Therefore, if you have set {#passphrase} in your global defaults,
311
+ # but wish to use {#passphrase_file} with a specific {Model}, be sure
312
+ # to clear {#passphrase} within that model's configuration.
313
+ #
314
+ # Backup::Encryptor::GPG.defaults do |enc|
315
+ # enc.passphrase = 'secret phrase'
316
+ # end
317
+ #
318
+ # Backup::Model.new(:my_backup, 'My Backup') do
319
+ # # other directives...
320
+ # encrypt_with GPG do |enc|
321
+ # enc.mode = :symmetric
322
+ # enc.passphrase = nil
323
+ # enc.passphrase_file = '/path/to/passphrase.file'
324
+ # end
325
+ # end
326
+ #
327
+ # @return [String]
328
+ attr_accessor :passphrase_file
329
+
330
+ ##
331
+ # Configures default accessor values for new class instances.
332
+ #
333
+ # If all required options are set, then no further configuration
334
+ # would be needed within a Model's definition when an Encryptor is added.
335
+ # Therefore, the following example is sufficient to encrypt +:my_backup+:
336
+ #
337
+ # # Defaults set in config.rb
338
+ # Backup::Encryptor::GPG.defaults do |encryptor|
339
+ # encryptor.keys = {}
340
+ # encryptor.keys['joe@example.com'] = <<-EOS
341
+ # -----BEGIN PGP PUBLIC KEY BLOCK-----
342
+ # Version: GnuPG v1.4.12 (GNU/Linux)
343
+ #
344
+ # mI0EUBR6CwEEAMVSlFtAXO4jXYnVFAWy6chyaMw+gXOFKlWojNXOOKmE3SujdLKh
345
+ # kWqnafx7VNrb8cjqxz6VZbumN9UgerFpusM3uLCYHnwyv/rGMf4cdiuX7gGltwGb
346
+ # (...etc...)
347
+ # mLekS3xntUhhgHKc4lhf4IVBqG4cFmwSZ0tZEJJUSESb3TqkkdnNLjE=
348
+ # =KEW+
349
+ # -----END PGP PUBLIC KEY BLOCK-----
350
+ # EOS
351
+ #
352
+ # encryptor.recipients = 'joe@example.com'
353
+ # end
354
+ #
355
+ # # Encryptor set in the model
356
+ # Backup::Model.new(:my_backup, 'My Backup') do
357
+ # # archives, storage options, etc...
358
+ # encrypt_with GPG
359
+ # end
360
+ #
361
+ # @!scope class
362
+ # @see Config::Helpers::ClassMethods#defaults
363
+ # @yield [config] OpenStruct object
364
+ # @!method defaults
365
+
366
+ ##
367
+ # Creates a new instance of Backup::Encryptor::GPG.
368
+ #
369
+ # This constructor is not used directly when configuring Backup.
370
+ # Use {Model#encrypt_with}.
371
+ #
372
+ # Model.new(:backup_trigger, 'Backup Label') do
373
+ # archive :my_archive do |archive|
374
+ # archive.add '/some/directory'
375
+ # end
376
+ #
377
+ # compress_with Gzip
378
+ #
379
+ # encrypt_with GPG do |encryptor|
380
+ # encryptor.mode = :both
381
+ # encryptor.passphrase = 'a secret'
382
+ # encryptor.recipients = ['joe@example.com', 'mary@example.com']
383
+ # end
384
+ #
385
+ # store_with SFTP
386
+ #
387
+ # notify_by Mail
388
+ # end
389
+ #
390
+ # @api private
391
+ def initialize(&block)
392
+ super
393
+
394
+ instance_eval(&block) if block_given?
395
+
396
+ @mode ||= :asymmetric
397
+ end
398
+
399
+ ##
400
+ # This is called as part of the procedure run by the Packager.
401
+ # It sets up the needed options to pass to the gpg command,
402
+ # then yields the command to use as part of the packaging procedure.
403
+ # Once the packaging procedure is complete, it will return
404
+ # so that any clean-up may be performed after the yield.
405
+ # Cleanup is also ensured, as temporary files may hold sensitive data.
406
+ # If no options can be built, the packaging process will be aborted.
407
+ #
408
+ # @api private
409
+ def encrypt_with
410
+ log!
411
+ prepare
412
+
413
+ if mode_options.empty?
414
+ raise Error, "Encryption could not be performed for mode '#{ mode }'"
415
+ end
416
+
417
+ yield "#{ utility(:gpg) } #{ base_options } #{ mode_options }", '.gpg'
418
+
419
+ ensure
420
+ cleanup
421
+ end
422
+
423
+ private
424
+
425
+ ##
426
+ # Remove any temporary directories and reset all instance variables.
427
+ #
428
+ def prepare
429
+ FileUtils.rm_rf(@tempdirs, :secure => true) if @tempdirs
430
+ @tempdirs = []
431
+ @base_options = nil
432
+ @mode_options = nil
433
+ @user_recipients = nil
434
+ @user_keys = nil
435
+ @system_identifiers = nil
436
+ end
437
+ alias :cleanup :prepare
438
+
439
+ ##
440
+ # Returns the options needed for the gpg command line which are
441
+ # not dependant on the #mode. --no-tty supresses output of certain
442
+ # messages, like the "Reading passphrase from file descriptor..."
443
+ # messages during symmetric encryption
444
+ #
445
+ def base_options
446
+ @base_options ||= begin
447
+ opts = ['--no-tty']
448
+ path = setup_gpg_homedir
449
+ opts << "--homedir '#{ path }'" if path
450
+ path = setup_gpg_config
451
+ opts << "--options '#{ path }'" if path
452
+ opts.join(' ')
453
+ end
454
+ end
455
+
456
+ ##
457
+ # Setup the given :gpg_homedir if needed, ensure the proper permissions
458
+ # are set, and return the directory's path. Otherwise, return false.
459
+ #
460
+ # If the GnuPG files do not exist, trigger their creation by requesting
461
+ # --list-secret-keys. Some commands, like for symmetric encryption, will
462
+ # issue messages about their creation on STDERR, which generates unwanted
463
+ # warnings in the log. This way, if any of these files are created here,
464
+ # we will get those messages on STDOUT for the log, without the actual
465
+ # secret key listing which we don't care about.
466
+ #
467
+ def setup_gpg_homedir
468
+ return false unless gpg_homedir
469
+
470
+ path = File.expand_path(gpg_homedir)
471
+ FileUtils.mkdir_p(path)
472
+ FileUtils.chown(Config.user, nil, path)
473
+ FileUtils.chmod(0700, path)
474
+
475
+ unless %w{ pubring.gpg secring.gpg trustdb.gpg }.
476
+ all? {|name| File.exist? File.join(path, name) }
477
+ run("#{ utility(:gpg) } --homedir '#{ path }' -K 2>&1 >/dev/null")
478
+ end
479
+
480
+ path
481
+
482
+ rescue => err
483
+ raise Error.wrap(
484
+ err, "Failed to create or set permissions for #gpg_homedir")
485
+ end
486
+
487
+ ##
488
+ # Write the given #gpg_config to a tempfile, within a tempdir, and
489
+ # return the file's path to be given to the gpg --options argument.
490
+ # If no #gpg_config is set, return false.
491
+ #
492
+ # This is required in order to set the proper permissions on the
493
+ # directory containing the tempfile. The tempdir will be removed
494
+ # after the packaging procedure is completed.
495
+ #
496
+ # Once written, we'll call check_gpg_config to make sure there are
497
+ # no problems that would prevent gpg from running with this config.
498
+ # If any errors occur during this process, we can not proceed.
499
+ # We'll cleanup to remove the tempdir (if created) and raise an error.
500
+ #
501
+ def setup_gpg_config
502
+ return false unless gpg_config
503
+
504
+ dir = Dir.mktmpdir('backup-gpg_config', Config.tmp_path)
505
+ @tempdirs << dir
506
+ file = Tempfile.open('backup-gpg_config', dir)
507
+ file.write gpg_config.gsub(/^[[:blank:]]+/, '')
508
+ file.close
509
+
510
+ check_gpg_config(file.path)
511
+
512
+ file.path
513
+
514
+ rescue => err
515
+ cleanup
516
+ raise Error.wrap(err, "Error creating temporary file for #gpg_config.")
517
+ end
518
+
519
+ ##
520
+ # Make sure the temporary GnuPG config file created from #gpg_config
521
+ # does not have any syntax errors that would prevent gpg from running.
522
+ # If so, raise the returned error message.
523
+ # Note that Cli::Helpers#run may also raise an error here.
524
+ #
525
+ def check_gpg_config(path)
526
+ ret = run(
527
+ "#{ utility(:gpg) } --options '#{ path }' --gpgconf-test 2>&1"
528
+ ).chomp
529
+ raise ret unless ret.empty?
530
+ end
531
+
532
+ ##
533
+ # Returns the options needed for the gpg command line to perform
534
+ # the encryption based on the #mode.
535
+ #
536
+ def mode_options
537
+ @mode_options ||= begin
538
+ s_opts = symmetric_options if mode != :asymmetric
539
+ a_opts = asymmetric_options if mode != :symmetric
540
+ [s_opts, a_opts].compact.join(' ')
541
+ end
542
+ end
543
+
544
+ ##
545
+ # Process :passphrase or :passphrase_file and return the command line
546
+ # options to perform symmetric encryption. If no :passphrase is
547
+ # specified, or an error occurs creating a temporary file for it, then
548
+ # try to use :passphrase_file if it's set.
549
+ # If the option can not be set, log a warning and return nil.
550
+ #
551
+ def symmetric_options
552
+ path = setup_passphrase_file
553
+ unless path || passphrase_file.to_s.empty?
554
+ path = File.expand_path(passphrase_file.to_s)
555
+ end
556
+
557
+ if path && File.exist?(path)
558
+ "-c --passphrase-file '#{ path }'"
559
+ else
560
+ Logger.warn("Symmetric encryption options could not be set.")
561
+ nil
562
+ end
563
+ end
564
+
565
+ ##
566
+ # Create a temporary file, within a tempdir, to hold the :passphrase and
567
+ # return the file's path. If an error occurs, log a warning.
568
+ # Return false if no :passphrase is set or an error occurs.
569
+ #
570
+ def setup_passphrase_file
571
+ return false if passphrase.to_s.empty?
572
+
573
+ dir = Dir.mktmpdir('backup-gpg_passphrase', Config.tmp_path)
574
+ @tempdirs << dir
575
+ file = Tempfile.open('backup-gpg_passphrase', dir)
576
+ file.write passphrase.to_s
577
+ file.close
578
+
579
+ file.path
580
+
581
+ rescue => err
582
+ Logger.warn Error.wrap(err, "Error creating temporary passphrase file.")
583
+ false
584
+ end
585
+
586
+ ##
587
+ # Process :recipients, importing their public key from :keys if needed,
588
+ # and return the command line options to perform asymmetric encryption.
589
+ # Log a warning and return nil if no valid recipients are found.
590
+ #
591
+ def asymmetric_options
592
+ if user_recipients.empty?
593
+ Logger.warn "No recipients available for asymmetric encryption."
594
+ nil
595
+ else
596
+ # skip trust database checks
597
+ "-e --trust-model always " +
598
+ user_recipients.map {|r| "-r '#{ r }'" }.join(' ')
599
+ end
600
+ end
601
+
602
+ ##
603
+ # Returns an Array of the public key identifiers the user specified
604
+ # in :recipients. Each identifier is 'cleaned' so that exact matches
605
+ # can be performed. Then each is checked to ensure it will find a
606
+ # public key that exists in the system's public keyring.
607
+ # If the identifier does not match an existing key, the public key
608
+ # associated with the identifier in :keys will be imported for use.
609
+ # If no key can be found in the system or in :keys for the identifier,
610
+ # a warning will be issued; as we will attempt to encrypt the backup
611
+ # and proceed if at all possible.
612
+ #
613
+ def user_recipients
614
+ @user_recipients ||= begin
615
+ [recipients].flatten.compact.map do |identifier|
616
+ identifier = clean_identifier(identifier)
617
+ if system_identifiers.include?(identifier)
618
+ identifier
619
+ else
620
+ key = user_keys[identifier]
621
+ if key
622
+ # will log a warning and return nil if the import fails
623
+ import_key(identifier, key)
624
+ else
625
+ Logger.warn(
626
+ "No public key was found in #keys for '#{ identifier }'"
627
+ )
628
+ nil
629
+ end
630
+ end
631
+ end.compact
632
+ end
633
+ end
634
+
635
+ ##
636
+ # Returns the #keys hash set by the user with all identifiers
637
+ # (Hash keys) 'cleaned' for exact matching. If the cleaning process
638
+ # creates duplicate keys, the user will be warned.
639
+ #
640
+ def user_keys
641
+ @user_keys ||= begin
642
+ _keys = keys || {}
643
+ ret = Hash[_keys.map {|k,v| [clean_identifier(k), v] }]
644
+ Logger.warn(
645
+ "Duplicate public key identifiers were detected in #keys."
646
+ ) if ret.keys.count != _keys.keys.count
647
+ ret
648
+ end
649
+ end
650
+
651
+ ##
652
+ # Cleans a public key identifier.
653
+ # Strip out all spaces, upcase non-email identifiers,
654
+ # and wrap email addresses in <> to perform exact matching.
655
+ #
656
+ def clean_identifier(str)
657
+ str = str.to_s.gsub(/[[:blank:]]+/, '')
658
+ str =~ /@/ ? "<#{ str.gsub(/(<|>)/,'') }>" : str.upcase
659
+ end
660
+
661
+ ##
662
+ # Import the given public key and return the 16 character Key ID.
663
+ # If the import fails, return nil.
664
+ # Note that errors raised by Cli::Helpers#run may also be rescued here.
665
+ #
666
+ def import_key(identifier, key)
667
+ file = Tempfile.open('backup-gpg_import', Config.tmp_path)
668
+ file.write(key.gsub(/^[[:blank:]]+/, ''))
669
+ file.close
670
+ ret = run(
671
+ "#{ utility(:gpg) } #{ base_options } " +
672
+ "--keyid-format 0xlong --import '#{ file.path }' 2>&1"
673
+ )
674
+ file.delete
675
+
676
+ keyid = ret.match(/ 0x(\w{16})/).to_a[1]
677
+ raise "GPG Returned:\n#{ ret.gsub(/^\s*/, ' ') }" unless keyid
678
+ keyid
679
+
680
+ rescue => err
681
+ Logger.warn Error.wrap(
682
+ err, "Public key import failed for '#{ identifier }'")
683
+ nil
684
+ end
685
+
686
+ ##
687
+ # Parse the information for all the public keys found in the public
688
+ # keyring (based on #gpg_homedir setting) and return an Array of all
689
+ # identifiers which could be used to specify a valid key.
690
+ #
691
+ def system_identifiers
692
+ @system_identifiers ||= begin
693
+ skip_key = false
694
+ data = run(
695
+ "#{ utility(:gpg) } #{ base_options } " +
696
+ "--with-colons --fixed-list-mode --fingerprint"
697
+ )
698
+ data.lines.map do |line|
699
+ line.strip!
700
+
701
+ # process public key record
702
+ if line =~ /^pub:/
703
+ validity, keyid, capabilities =
704
+ line.split(':').values_at(1, 4, 11)
705
+ # skip keys marked as revoked ('r'), expired ('e'),
706
+ # invalid ('i') or disabled ('D')
707
+ if validity[0,1] =~ /(r|e|i)/ || capabilities =~ /D/
708
+ skip_key = true
709
+ next nil
710
+ else
711
+ skip_key = false
712
+ # return both the long and short id
713
+ next [keyid[-8..-1], keyid]
714
+ end
715
+ else
716
+ # wait for the next valid public key record
717
+ next nil if skip_key
718
+
719
+ # process UID records for the current public key
720
+ if line =~ /^uid:/
721
+ validity, userid = line.split(':').values_at(1, 9)
722
+ # skip records marked as revoked ('r'), expired ('e')
723
+ # or invalid ('i')
724
+ if validity !~ /(r|e|i)/
725
+ # return the last email found in user id string,
726
+ # since this includes user supplied comments.
727
+ # return nil if no email found.
728
+ email, str = nil, userid
729
+ while match = str.match(/<.+?@.+?>/)
730
+ email, str = match[0], match.post_match
731
+ end
732
+ next email
733
+ end
734
+ # return public key's fingerprint
735
+ elsif line =~ /^fpr:/
736
+ next line.split(':')[9]
737
+ end
738
+
739
+ nil # ignore any other lines
740
+ end
741
+ end.flatten.compact
742
+ end
743
+ end
744
+
745
+ end
746
+ end
747
+ end