backup 3.0.26 → 3.0.27

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