backupii 0.1.0.pre.alpha.1

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