hiera-eyaml 1.1.2 → 1.1.3
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 +4 -4
- data/bin/eyaml +246 -56
- data/lib/hiera/backend/version.rb +1 -1
- metadata +1 -1
data/README.md
CHANGED
@@ -47,9 +47,9 @@ This creates a public and private key with default names in the default location
|
|
47
47
|
|
48
48
|
To encrypt something, you only need the public_key, so distribute that to people creating hiera properties
|
49
49
|
|
50
|
-
$ eyaml -e
|
50
|
+
$ eyaml -e filename # Encrypt a file
|
51
|
+
$ eyaml -e -s text # Encrypt some text
|
51
52
|
$ eyaml -e -p # Encrypt a password (prompt for it)
|
52
|
-
$ eyaml -e -f filename # Encrypt a file
|
53
53
|
|
54
54
|
### Decryption
|
55
55
|
|
@@ -57,8 +57,8 @@ This creates a public and private key with default names in the default location
|
|
57
57
|
|
58
58
|
To test decryption you can also use the eyaml tool if you have both keys
|
59
59
|
|
60
|
-
$ eyaml -d
|
61
|
-
$ eyaml -d -
|
60
|
+
$ eyaml -d filename # Decrypt a file (PEM format)
|
61
|
+
$ eyaml -d -s SOME-ENCRYPTED-TEXT # Decrypt some text
|
62
62
|
|
63
63
|
Change the permissions so that the private key is only readable by the user that hiera (puppet) is
|
64
64
|
running as.
|
data/bin/eyaml
CHANGED
@@ -5,8 +5,14 @@ require 'base64'
|
|
5
5
|
require 'trollop'
|
6
6
|
require 'highline/import'
|
7
7
|
require 'hiera/backend/version'
|
8
|
+
require 'tempfile'
|
8
9
|
|
9
|
-
|
10
|
+
ENCRYPTED_BLOCK = />\n( *)ENC\[([a-zA-Z0-9+\/ \n]+)\]/
|
11
|
+
ENCRYPTED_STRING = /ENC\[([a-zA-Z0-9+\/]+)\]/
|
12
|
+
DECRYPTED_BLOCK = />\n( *)ENC!\[(.+)\]!ENC/
|
13
|
+
DECRYPTED_STRING = /ENC!\[(.+)\]!ENC/
|
14
|
+
|
15
|
+
def ensure_key_dir_exists(key_file)
|
10
16
|
key_dir = File.dirname(key_file)
|
11
17
|
|
12
18
|
if !File.directory?(key_dir)
|
@@ -15,6 +21,73 @@ def ensureKeyDirExists(key_file)
|
|
15
21
|
end
|
16
22
|
end
|
17
23
|
|
24
|
+
def get_file_input(options)
|
25
|
+
if options[:file]
|
26
|
+
File.read options[:file]
|
27
|
+
else
|
28
|
+
STDIN.read
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_input(options)
|
33
|
+
return options[:string] if options[:string]
|
34
|
+
|
35
|
+
if options[:password]
|
36
|
+
ask("Enter password: ") {|q| q.echo = "*" }
|
37
|
+
end
|
38
|
+
|
39
|
+
get_file_input(options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def encrypt(public_key, plaintext)
|
43
|
+
cipher = OpenSSL::Cipher::AES.new(256, :CBC)
|
44
|
+
ciphertext_binary = OpenSSL::PKCS7::encrypt([public_key], plaintext, cipher, OpenSSL::PKCS7::BINARY).to_der
|
45
|
+
Base64.encode64(ciphertext_binary).strip
|
46
|
+
end
|
47
|
+
|
48
|
+
def decrypt(public_key, private_key, ciphertext)
|
49
|
+
ciphertext_decoded = Base64.decode64(ciphertext)
|
50
|
+
pkcs7 = OpenSSL::PKCS7.new( ciphertext_decoded )
|
51
|
+
pkcs7.decrypt(private_key, public_key)
|
52
|
+
end
|
53
|
+
|
54
|
+
def decrypt_eyaml(public_key, private_key, cipher_eyaml)
|
55
|
+
# decrypt blocks first
|
56
|
+
plain_block_eyaml = cipher_eyaml.gsub(ENCRYPTED_BLOCK) { |match|
|
57
|
+
indentation = $1
|
58
|
+
ciphertext = $2.gsub(/[ \n]/, '')
|
59
|
+
plaintext = decrypt(public_key, private_key, ciphertext)
|
60
|
+
">\n"+indentation+"ENC![" + plaintext + "]!ENC"
|
61
|
+
}
|
62
|
+
# then decrypt strings
|
63
|
+
plain_block_eyaml.gsub(ENCRYPTED_STRING) { |match|
|
64
|
+
plaintext = decrypt(public_key, private_key, $1)
|
65
|
+
"ENC![" + plaintext + "]!ENC"
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def encrypt_eyaml(public_key, plain_eyaml)
|
70
|
+
# encrypt blocks
|
71
|
+
cipher_block_eyaml = plain_eyaml.gsub(DECRYPTED_BLOCK) { |match|
|
72
|
+
indentation = $1
|
73
|
+
ciphertext = encrypt(public_key, $2).gsub(/\n/,"\n"+indentation)
|
74
|
+
">\n" + indentation + "ENC[" + ciphertext + "]"
|
75
|
+
}
|
76
|
+
# encrypt strings
|
77
|
+
cipher_block_eyaml.gsub(DECRYPTED_STRING) { |match|
|
78
|
+
ciphertext = encrypt(public_key, $1).gsub(/\n/,'')
|
79
|
+
"ENC[" + ciphertext + "]"
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def shred(file, clean_size)
|
84
|
+
[0xff, 0x55, 0xaa, 0x00].each do |byte|
|
85
|
+
file.seek(0, IO::SEEK_SET)
|
86
|
+
clean_size.times { file.print(byte.chr) }
|
87
|
+
file.fsync
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
18
91
|
options = Trollop::options do
|
19
92
|
|
20
93
|
version "Hiera-eyaml version " + Hiera::Backend::Eyaml::VERSION.to_s
|
@@ -22,28 +95,33 @@ options = Trollop::options do
|
|
22
95
|
Hiera-eyaml is a backend for Hiera which provides OpenSSL encryption/decryption for Hiera properties
|
23
96
|
|
24
97
|
Usage:
|
25
|
-
eyaml [options] [
|
98
|
+
eyaml [options] [file-to-encrypt]
|
26
99
|
EOS
|
27
100
|
|
28
101
|
opt :createkeys, "Create public and private keys for use encrypting properties", :short => 'c'
|
102
|
+
opt :encrypt, "Encrypt a string, password, file or stdin"
|
103
|
+
opt :decrypt, "Decrypt a string, file or stdin"
|
104
|
+
opt :eyaml, "Assume input is eyaml format"
|
105
|
+
opt :edit, "Edit an encrypted file inplace, implies --eyaml"
|
29
106
|
opt :password, "Encrypt a password entered on the terminal", :short => 'p'
|
30
|
-
opt :
|
31
|
-
opt :private_key, "Filename of the private_key", :type => :string
|
32
|
-
opt :public_key, "Filename of the public_key", :type => :string
|
33
|
-
opt :
|
34
|
-
opt :decrypt, "Decrypt something"
|
107
|
+
opt :string, "Encrypt a string provided on the command line", :short => 's', :type => :string
|
108
|
+
opt :private_key, "Filename of the private_key", :type => :string, :default => "/etc/hiera/keys/private_key.pem"
|
109
|
+
opt :public_key, "Filename of the public_key", :type => :string, :default => "/etc/hiera/keys/public_key.pem"
|
110
|
+
opt :output, "Output mode to use when encrypting (examples, block or string)", :type => :string, :default => "examples"
|
35
111
|
end
|
36
112
|
|
37
|
-
|
113
|
+
main_option_count = 0
|
114
|
+
main_option_count += 1 if options[:createkeys]
|
115
|
+
main_option_count += 1 if options[:encrypt]
|
116
|
+
main_option_count += 1 if options[:decrypt]
|
117
|
+
main_option_count += 1 if options[:edit]
|
38
118
|
|
39
|
-
|
40
|
-
options[:
|
41
|
-
options[:public_key] ||= "/etc/hiera/keys/public_key.pem"
|
42
|
-
options[:string] = ARGV.join(' ')
|
119
|
+
Trollop::die "You can only specify one main action" if main_option_count > 1
|
120
|
+
Trollop::die "You cannot specify --password and --string" if options[:password] and options[:string]
|
43
121
|
|
44
|
-
|
45
|
-
|
46
|
-
|
122
|
+
options[:file] = ARGV[0]
|
123
|
+
if options[:file] and (options[:password] or options[:string])
|
124
|
+
$stderr.puts "WARN: file supplied but will be shadowed by string or password"
|
47
125
|
end
|
48
126
|
|
49
127
|
if options[:createkeys]
|
@@ -51,15 +129,15 @@ if options[:createkeys]
|
|
51
129
|
# Try to do equivalent of:
|
52
130
|
# openssl req -x509 -nodes -days 100000 -newkey rsa:2048 -keyout privatekey.pem -out publickey.pem -subj '/'
|
53
131
|
|
54
|
-
|
55
|
-
|
132
|
+
ensure_key_dir_exists(options[:private_key])
|
133
|
+
ensure_key_dir_exists(options[:public_key])
|
56
134
|
|
57
135
|
key = OpenSSL::PKey::RSA.new(2048)
|
58
136
|
open( options[:private_key], "w" ) do |io|
|
59
137
|
io.write(key.to_pem)
|
60
138
|
end
|
61
139
|
|
62
|
-
puts "#{options[:private_key]} created."
|
140
|
+
$stderr.puts "#{options[:private_key]} created."
|
63
141
|
|
64
142
|
name = OpenSSL::X509::Name.parse("/")
|
65
143
|
cert = OpenSSL::X509::Certificate.new()
|
@@ -85,68 +163,180 @@ if options[:createkeys]
|
|
85
163
|
open( options[:public_key], "w" ) do |io|
|
86
164
|
io.write(cert.to_pem)
|
87
165
|
end
|
88
|
-
puts "#{options[:public_key]} created."
|
166
|
+
$stderr.puts "#{options[:public_key]} created."
|
89
167
|
exit
|
90
168
|
end
|
91
169
|
|
92
|
-
if options[:
|
170
|
+
if options[:eyaml]
|
93
171
|
|
94
|
-
|
95
|
-
|
96
|
-
|
172
|
+
if options[:decrypt]
|
173
|
+
# prepare to decrypt blocks
|
174
|
+
private_key_pem = File.read( options[:private_key] )
|
175
|
+
private_key = OpenSSL::PKey::RSA.new( private_key_pem )
|
97
176
|
|
98
|
-
|
99
|
-
|
177
|
+
public_key_pem = File.read( options[:public_key] )
|
178
|
+
public_key = OpenSSL::X509::Certificate.new( public_key_pem )
|
179
|
+
|
180
|
+
eyaml = get_file_input options
|
181
|
+
plain_eyaml = decrypt_eyaml(public_key, private_key, eyaml)
|
182
|
+
|
183
|
+
puts plain_eyaml
|
100
184
|
exit
|
101
185
|
end
|
102
186
|
|
103
|
-
|
104
|
-
|
187
|
+
if options[:encrypt]
|
188
|
+
# prepare to encrypt blocks
|
189
|
+
public_key_pem = File.read( options[:public_key] )
|
190
|
+
public_key = OpenSSL::X509::Certificate.new( public_key_pem )
|
105
191
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
192
|
+
eyaml = get_file_input options
|
193
|
+
cipher_eyaml = encrypt_yaml(public_key, eyaml)
|
194
|
+
|
195
|
+
puts eyaml
|
196
|
+
exit
|
197
|
+
end
|
110
198
|
|
111
|
-
|
112
|
-
puts "block: >"
|
113
|
-
puts " ENC[" + ciphertext_as_block.gsub(/\n/, "\n ") + "]"
|
114
|
-
exit
|
199
|
+
else
|
115
200
|
|
116
|
-
|
201
|
+
if options[:encrypt]
|
202
|
+
plaintext = get_input options
|
117
203
|
|
118
|
-
if
|
204
|
+
if plaintext.nil? or plaintext.length == 0
|
205
|
+
$stderr.puts "Specify a string or --file to encrypt something. See --help for more usage instructions."
|
206
|
+
exit
|
207
|
+
end
|
119
208
|
|
120
|
-
|
121
|
-
|
122
|
-
|
209
|
+
public_key_pem = File.read( options[:public_key] )
|
210
|
+
public_key = OpenSSL::X509::Certificate.new( public_key_pem )
|
211
|
+
|
212
|
+
ciphertext_as_block = encrypt(public_key, plaintext)
|
213
|
+
ciphertext_as_string = ciphertext_as_block.split("\n").join('')
|
214
|
+
|
215
|
+
case options[:output]
|
216
|
+
when "examples"
|
217
|
+
puts "string: ENC[#{ciphertext_as_string}]\n\nOR\n\n"
|
218
|
+
puts "block: >"
|
219
|
+
puts " ENC[" + ciphertext_as_block.gsub(/\n/, "\n ") + "]"
|
220
|
+
when "block"
|
221
|
+
puts "ENC[" + ciphertext_as_block + "]"
|
222
|
+
when "string"
|
223
|
+
puts "ENC[#{ciphertext_as_string}]"
|
224
|
+
else
|
225
|
+
$stderr.puts "Unknown output option: " + options[:output]
|
226
|
+
exit 1
|
227
|
+
end
|
228
|
+
exit
|
123
229
|
|
124
|
-
|
230
|
+
end
|
125
231
|
|
126
|
-
|
127
|
-
ciphertext_decoded = Base64.decode64(ciphertext)
|
232
|
+
if options[:decrypt]
|
128
233
|
|
129
|
-
|
130
|
-
|
131
|
-
|
234
|
+
ciphertext = get_input options
|
235
|
+
if ciphertext.nil? or ciphertext.length == 0
|
236
|
+
$stderr.puts "Specify a string or --file to decrypt something. See --help for more usage instructions."
|
237
|
+
exit 1
|
132
238
|
end
|
133
239
|
|
134
|
-
|
135
|
-
private_key = OpenSSL::PKey::RSA.new( private_key_pem )
|
240
|
+
if ciphertext.start_with? "ENC["
|
136
241
|
|
137
|
-
|
138
|
-
public_key = OpenSSL::X509::Certificate.new( public_key_pem )
|
242
|
+
ciphertext = ciphertext[4..-2]
|
139
243
|
|
140
|
-
|
244
|
+
private_key_pem = File.read( options[:private_key] )
|
245
|
+
private_key = OpenSSL::PKey::RSA.new( private_key_pem )
|
246
|
+
|
247
|
+
public_key_pem = File.read( options[:public_key] )
|
248
|
+
public_key = OpenSSL::X509::Certificate.new( public_key_pem )
|
249
|
+
|
250
|
+
plaintext = decrypt(public_key, private_key, ciphertext)
|
251
|
+
puts "#{plaintext}"
|
252
|
+
exit
|
253
|
+
|
254
|
+
else
|
255
|
+
|
256
|
+
$stderr.puts "Ciphertext is not an eyaml encrypted string (Does not start with ENC[...])"
|
257
|
+
exit 1
|
258
|
+
|
259
|
+
end
|
141
260
|
|
142
|
-
plaintext = pkcs7.decrypt(private_key, public_key)
|
143
|
-
puts "#{plaintext}"
|
144
261
|
exit
|
145
262
|
|
146
|
-
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
147
266
|
|
148
|
-
|
267
|
+
if options[:edit]
|
268
|
+
$stderr.puts "Launching edit mode"
|
269
|
+
# prepare to edit blocks
|
270
|
+
private_key_pem = File.read( options[:private_key] )
|
271
|
+
private_key = OpenSSL::PKey::RSA.new( private_key_pem )
|
149
272
|
|
273
|
+
public_key_pem = File.read( options[:public_key] )
|
274
|
+
public_key = OpenSSL::X509::Certificate.new( public_key_pem )
|
275
|
+
|
276
|
+
original_eyaml = File.read( options[:file] )
|
277
|
+
original_size = original_eyaml.length
|
278
|
+
plain_eyaml = decrypt_eyaml(public_key, private_key, original_eyaml)
|
279
|
+
|
280
|
+
# write temp file
|
281
|
+
plain_file = Tempfile.open('eyaml_edit')
|
282
|
+
plain_file.puts plain_eyaml
|
283
|
+
plain_file.flush
|
284
|
+
|
285
|
+
# open editor
|
286
|
+
$editor = ENV['EDITOR']
|
287
|
+
if $editor == nil
|
288
|
+
%w{/usr/bin/sensible-editor /usr/bin/editor /usr/bin/vim /usr/bin/vi}.each do |editor|
|
289
|
+
if FileTest.executable?(editor)
|
290
|
+
$editor = editor
|
291
|
+
break
|
292
|
+
end
|
293
|
+
end
|
150
294
|
end
|
295
|
+
$stderr.puts "Opening decrypted file for editing in " + $editor
|
296
|
+
system($editor, plain_file.path)
|
297
|
+
status = $?
|
151
298
|
|
152
|
-
|
299
|
+
throw "Process has not exited!?" unless status.exited?
|
300
|
+
|
301
|
+
unless status.exitstatus == 0
|
302
|
+
$syserr.puts "Editor did not exit successfully (exit code #{status.exitstatus}), aborting"
|
303
|
+
exit 1
|
304
|
+
end
|
305
|
+
|
306
|
+
# some editors do not write new content in place, but instead
|
307
|
+
# make a new file and more it in the old file's place.
|
308
|
+
begin
|
309
|
+
reopened_plain_file = File.open(plain_file.path, "r+")
|
310
|
+
rescue Exception => e
|
311
|
+
STDERR.puts e
|
312
|
+
exit 1
|
313
|
+
end
|
314
|
+
new_eyaml = reopened_plain_file.read
|
315
|
+
|
316
|
+
# make sure nothing is left on disk
|
317
|
+
new_size = new_eyaml.length
|
318
|
+
old_size = plain_eyaml.length
|
319
|
+
clear_size = (new_size > old_size) ? new_size : old_size
|
320
|
+
shred(plain_file, clear_size)
|
321
|
+
plain_file.close true
|
322
|
+
shred(reopened_plain_file, clear_size)
|
323
|
+
reopened_plain_file.close
|
324
|
+
|
325
|
+
if new_eyaml.length == 0
|
326
|
+
$stderr.puts "Replacement content is empty, aborting"
|
327
|
+
exit 1
|
328
|
+
end
|
329
|
+
|
330
|
+
if (new_eyaml == plain_eyaml)
|
331
|
+
$stderr.puts "No changes made, aborting"
|
332
|
+
exit 1
|
333
|
+
end
|
334
|
+
|
335
|
+
# re-encrypt the file again, currently we re-encrypt indiscriminately
|
336
|
+
cipher_eyaml = encrypt_eyaml(public_key, new_eyaml)
|
337
|
+
|
338
|
+
File.open(options[:file], 'w') { |file|
|
339
|
+
file.write(cipher_eyaml)
|
340
|
+
}
|
341
|
+
exit
|
342
|
+
end
|