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.
- checksums.yaml +7 -0
- data/LICENSE +19 -0
- data/README.md +37 -0
- data/bin/backupii +5 -0
- data/bin/docker_test +24 -0
- data/lib/backup/archive.rb +171 -0
- data/lib/backup/binder.rb +23 -0
- data/lib/backup/cleaner.rb +114 -0
- data/lib/backup/cli.rb +376 -0
- data/lib/backup/cloud_io/base.rb +40 -0
- data/lib/backup/cloud_io/cloud_files.rb +301 -0
- data/lib/backup/cloud_io/s3.rb +256 -0
- data/lib/backup/compressor/base.rb +34 -0
- data/lib/backup/compressor/bzip2.rb +37 -0
- data/lib/backup/compressor/custom.rb +51 -0
- data/lib/backup/compressor/gzip.rb +76 -0
- data/lib/backup/config/dsl.rb +103 -0
- data/lib/backup/config/helpers.rb +139 -0
- data/lib/backup/config.rb +122 -0
- data/lib/backup/database/base.rb +89 -0
- data/lib/backup/database/mongodb.rb +189 -0
- data/lib/backup/database/mysql.rb +194 -0
- data/lib/backup/database/openldap.rb +97 -0
- data/lib/backup/database/postgresql.rb +134 -0
- data/lib/backup/database/redis.rb +179 -0
- data/lib/backup/database/riak.rb +82 -0
- data/lib/backup/database/sqlite.rb +57 -0
- data/lib/backup/encryptor/base.rb +29 -0
- data/lib/backup/encryptor/gpg.rb +745 -0
- data/lib/backup/encryptor/open_ssl.rb +76 -0
- data/lib/backup/errors.rb +55 -0
- data/lib/backup/logger/console.rb +50 -0
- data/lib/backup/logger/fog_adapter.rb +27 -0
- data/lib/backup/logger/logfile.rb +134 -0
- data/lib/backup/logger/syslog.rb +116 -0
- data/lib/backup/logger.rb +199 -0
- data/lib/backup/model.rb +478 -0
- data/lib/backup/notifier/base.rb +128 -0
- data/lib/backup/notifier/campfire.rb +63 -0
- data/lib/backup/notifier/command.rb +101 -0
- data/lib/backup/notifier/datadog.rb +107 -0
- data/lib/backup/notifier/flowdock.rb +101 -0
- data/lib/backup/notifier/hipchat.rb +118 -0
- data/lib/backup/notifier/http_post.rb +116 -0
- data/lib/backup/notifier/mail.rb +235 -0
- data/lib/backup/notifier/nagios.rb +67 -0
- data/lib/backup/notifier/pagerduty.rb +82 -0
- data/lib/backup/notifier/prowl.rb +70 -0
- data/lib/backup/notifier/pushover.rb +73 -0
- data/lib/backup/notifier/ses.rb +126 -0
- data/lib/backup/notifier/slack.rb +149 -0
- data/lib/backup/notifier/twitter.rb +57 -0
- data/lib/backup/notifier/zabbix.rb +62 -0
- data/lib/backup/package.rb +53 -0
- data/lib/backup/packager.rb +108 -0
- data/lib/backup/pipeline.rb +122 -0
- data/lib/backup/splitter.rb +75 -0
- data/lib/backup/storage/base.rb +72 -0
- data/lib/backup/storage/cloud_files.rb +158 -0
- data/lib/backup/storage/cycler.rb +73 -0
- data/lib/backup/storage/dropbox.rb +208 -0
- data/lib/backup/storage/ftp.rb +118 -0
- data/lib/backup/storage/local.rb +63 -0
- data/lib/backup/storage/qiniu.rb +68 -0
- data/lib/backup/storage/rsync.rb +251 -0
- data/lib/backup/storage/s3.rb +157 -0
- data/lib/backup/storage/scp.rb +67 -0
- data/lib/backup/storage/sftp.rb +82 -0
- data/lib/backup/syncer/base.rb +70 -0
- data/lib/backup/syncer/cloud/base.rb +180 -0
- data/lib/backup/syncer/cloud/cloud_files.rb +83 -0
- data/lib/backup/syncer/cloud/local_file.rb +99 -0
- data/lib/backup/syncer/cloud/s3.rb +118 -0
- data/lib/backup/syncer/rsync/base.rb +55 -0
- data/lib/backup/syncer/rsync/local.rb +29 -0
- data/lib/backup/syncer/rsync/pull.rb +49 -0
- data/lib/backup/syncer/rsync/push.rb +206 -0
- data/lib/backup/template.rb +45 -0
- data/lib/backup/utilities.rb +235 -0
- data/lib/backup/version.rb +5 -0
- data/lib/backup.rb +141 -0
- data/templates/cli/archive +28 -0
- data/templates/cli/compressor/bzip2 +4 -0
- data/templates/cli/compressor/custom +7 -0
- data/templates/cli/compressor/gzip +4 -0
- data/templates/cli/config +123 -0
- data/templates/cli/databases/mongodb +15 -0
- data/templates/cli/databases/mysql +18 -0
- data/templates/cli/databases/openldap +24 -0
- data/templates/cli/databases/postgresql +16 -0
- data/templates/cli/databases/redis +16 -0
- data/templates/cli/databases/riak +17 -0
- data/templates/cli/databases/sqlite +11 -0
- data/templates/cli/encryptor/gpg +27 -0
- data/templates/cli/encryptor/openssl +9 -0
- data/templates/cli/model +26 -0
- data/templates/cli/notifier/zabbix +15 -0
- data/templates/cli/notifiers/campfire +12 -0
- data/templates/cli/notifiers/command +32 -0
- data/templates/cli/notifiers/datadog +57 -0
- data/templates/cli/notifiers/flowdock +16 -0
- data/templates/cli/notifiers/hipchat +16 -0
- data/templates/cli/notifiers/http_post +32 -0
- data/templates/cli/notifiers/mail +24 -0
- data/templates/cli/notifiers/nagios +13 -0
- data/templates/cli/notifiers/pagerduty +12 -0
- data/templates/cli/notifiers/prowl +11 -0
- data/templates/cli/notifiers/pushover +11 -0
- data/templates/cli/notifiers/ses +15 -0
- data/templates/cli/notifiers/slack +22 -0
- data/templates/cli/notifiers/twitter +13 -0
- data/templates/cli/splitter +7 -0
- data/templates/cli/storages/cloud_files +11 -0
- data/templates/cli/storages/dropbox +20 -0
- data/templates/cli/storages/ftp +13 -0
- data/templates/cli/storages/local +8 -0
- data/templates/cli/storages/qiniu +12 -0
- data/templates/cli/storages/rsync +17 -0
- data/templates/cli/storages/s3 +16 -0
- data/templates/cli/storages/scp +15 -0
- data/templates/cli/storages/sftp +15 -0
- data/templates/cli/syncers/cloud_files +22 -0
- data/templates/cli/syncers/rsync_local +20 -0
- data/templates/cli/syncers/rsync_pull +28 -0
- data/templates/cli/syncers/rsync_push +28 -0
- data/templates/cli/syncers/s3 +27 -0
- data/templates/general/links +3 -0
- data/templates/general/version.erb +2 -0
- data/templates/notifier/mail/failure.erb +16 -0
- data/templates/notifier/mail/success.erb +16 -0
- data/templates/notifier/mail/warning.erb +16 -0
- data/templates/storage/dropbox/authorization_url.erb +6 -0
- data/templates/storage/dropbox/authorized.erb +4 -0
- data/templates/storage/dropbox/cache_file_written.erb +10 -0
- 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
|