hiera-eyaml 1.1.2 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.md +4 -4
  2. data/bin/eyaml +246 -56
  3. data/lib/hiera/backend/version.rb +1 -1
  4. 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 text # Encrypt some text
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 SOME-ENCRYPTED-TEXT # Decrypt some text
61
- $ eyaml -d -f filename # Decrypt a file (PEM format)
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
- def ensureKeyDirExists(key_file)
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] [string-to-encrypt]
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 :file, "Encrypt a file instead of a string", :short => 'f', :type => :string
31
- opt :private_key, "Filename of the private_key", :type => :string
32
- opt :public_key, "Filename of the public_key", :type => :string
33
- opt :encrypt, "Encrypt something"
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
- Trollop::die "You cannot specify --encrypt and --decrypt" if options[:encrypt] and options[:decrypt]
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
- # Defaults
40
- options[:private_key] ||= "/etc/hiera/keys/private_key.pem"
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
- if options[:password]
45
- password = ask("Enter password: ") {|q| q.echo = "*" }
46
- options[:string] = password
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
- ensureKeyDirExists(options[:private_key])
55
- ensureKeyDirExists(options[:public_key])
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[:encrypt]
170
+ if options[:eyaml]
93
171
 
94
- plaintext = nil
95
- plaintext = options[:string] if options[:string]
96
- plaintext = File.read( options[:file] ) if options[:file]
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
- if plaintext.nil?
99
- puts "Specify a string or --file to encrypt something. See --help for more usage instructions."
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
- public_key_pem = File.read( options[:public_key] )
104
- public_key = OpenSSL::X509::Certificate.new( public_key_pem )
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
- cipher = OpenSSL::Cipher::AES.new(256, :CBC)
107
- ciphertext_binary = OpenSSL::PKCS7::encrypt([public_key], plaintext, cipher, OpenSSL::PKCS7::BINARY).to_der
108
- ciphertext_as_block = Base64.encode64(ciphertext_binary).strip
109
- ciphertext_as_string = ciphertext_as_block.split("\n").join('')
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
- puts "string: ENC[#{ciphertext_as_string}]\n\nOR\n\n"
112
- puts "block: >"
113
- puts " ENC[" + ciphertext_as_block.gsub(/\n/, "\n ") + "]"
114
- exit
199
+ else
115
200
 
116
- end
201
+ if options[:encrypt]
202
+ plaintext = get_input options
117
203
 
118
- if options[:decrypt]
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
- ciphertext = nil
121
- ciphertext = options[:string] if options[:string]
122
- ciphertext = File.read( options[:file] ) if options[:file]
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
- if ciphertext.start_with? "ENC["
230
+ end
125
231
 
126
- ciphertext = ciphertext[4..-2]
127
- ciphertext_decoded = Base64.decode64(ciphertext)
232
+ if options[:decrypt]
128
233
 
129
- if ciphertext.nil?
130
- puts "Specify a string or --file to decrypt something. See --help for more usage instructions."
131
- exit
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
- private_key_pem = File.read( options[:private_key] )
135
- private_key = OpenSSL::PKey::RSA.new( private_key_pem )
240
+ if ciphertext.start_with? "ENC["
136
241
 
137
- public_key_pem = File.read( options[:public_key] )
138
- public_key = OpenSSL::X509::Certificate.new( public_key_pem )
242
+ ciphertext = ciphertext[4..-2]
139
243
 
140
- pkcs7 = OpenSSL::PKCS7.new( ciphertext_decoded )
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
- else
263
+ end
264
+ end
265
+
147
266
 
148
- puts "Ciphertext is not an eyaml encrypted string (Does not start with ENC[...])"
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
- end
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
@@ -1,7 +1,7 @@
1
1
  module Hiera
2
2
  module Backend
3
3
  module Eyaml
4
- VERSION = "1.1.2"
4
+ VERSION = "1.1.3"
5
5
  end
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiera-eyaml
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.1.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: