veil 0.2.0 → 0.3.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: cfc065388eeda8de97ced25becd31b968eaebd1c
4
- data.tar.gz: 4f254696b35ccd7924bb84db6a18b65629570533
2
+ SHA256:
3
+ metadata.gz: 90b6d3458208ed72b315d558278ff129d88328b61440ef8be942df67737e821c
4
+ data.tar.gz: 452932d08b0483b6153ea3e7d0eace13acbdcf41b53ed0fecb5ea97aef518dc7
5
5
  SHA512:
6
- metadata.gz: 858dd8cba868d10bc3559b7ea6e4467a6cbeb9a3316244c0289b87adeef13709306a9c909d8dd8b1a1593b54a3541dd7cda1b3dce6ece469c937db2e83990270
7
- data.tar.gz: 831847b81b49f810c5b37e4f7a4211723f9fa84bcf5a26cce62a76ef9861d7288cd31bcfea08992aaee068b001c157ed1757e78311487f98d330cdc68d5fdea7
6
+ metadata.gz: 16deb81b28bb2dbd252cbd05269f2488296e24689786c037043a2b5ce7d3af0cef583ffe912fdfd22b70c5d9ef4b22acdfa4ee57bdf90865438cfbef2ab35f5e
7
+ data.tar.gz: 7a20341b3e3061f9907d419fbb37f94b4b0690786b1d90dc943865ce235e620619fef290a315e769d96e6b4ab132b5be2620e970faa41d1f7b6d26bf60e42f73
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ require 'veil'
3
+ require 'json'
4
+ require 'optparse'
5
+
6
+ OptionParser.new do |opts|
7
+ opts.banner = "Usage: veil-dump-secrets SECRETS_FILE_PATH"
8
+ end.parse!
9
+
10
+ secrets_file = ARGV[0]
11
+ veil = Veil::CredentialCollection::ChefSecretsFile.from_file(secrets_file)
12
+ puts veil.credentials_for_export.to_json
data/bin/veil-env-helper CHANGED
@@ -1,10 +1,16 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'veil'
4
+ require 'json'
4
5
  require 'optparse'
5
6
 
6
7
  options = {
7
- secrets_file: "/etc/opscode/private-chef-secrets.json"
8
+ secrets_file: "/etc/opscode/private-chef-secrets.json",
9
+ pack: false,
10
+ use_file: false,
11
+ debug: false,
12
+ secrets: [],
13
+ optional_secrets: []
8
14
  }
9
15
 
10
16
  OptionParser.new do |opts|
@@ -14,8 +20,20 @@ OptionParser.new do |opts|
14
20
  options[:debug] = d
15
21
  end
16
22
 
17
- opts.on("-s SECRETS_SPEC", "--secrets SECRETS_SPEC", "A comma seperated list of secrets to put in the environment") do |spec|
18
- options[:secrets] = spec.split(",")
23
+ opts.on("--pack", "Pass secrets in a single CHEF_SECRETS_DATA environment variable") do |p|
24
+ options[:pack] = p
25
+ end
26
+
27
+ opts.on("--use-file", "Pass secrets via a unlinked file available at the FD specified in CHEF_SECRETS_FD") do |fd|
28
+ options[:use_file] = fd
29
+ end
30
+
31
+ opts.on("-s SECRET_SPEC", "--secret SECRET_SPEC", "Secret to put in the environment") do |spec|
32
+ options[:secrets] << spec
33
+ end
34
+
35
+ opts.on("-o SECRET_SPEC", "--optional-secret SECRET_SPEC", "Optional secrets to put in the environment (if it exists)") do |spec|
36
+ options[:optional_secrets] << spec
19
37
  end
20
38
 
21
39
  opts.on("-f SECRETS_FILE", "--secrets-file SECRETS_FILE", "Location of veil-managed secrets file. Default: /etc/opscode/private-chef-secrets.json") do |file|
@@ -23,25 +41,64 @@ OptionParser.new do |opts|
23
41
  end
24
42
  end.parse!
25
43
 
26
- def env_name_from_secret_spec(secret)
44
+ def from_secret_spec(secret, start = {})
27
45
  parts = secret.split("=")
28
- case parts.length
29
- when 1
30
- ["CHEF_SECRET_#{parts[0].upcase}", parts[0]]
31
- when 2
32
- [parts[0].upcase, parts[1]]
46
+ env_name, secret_name = case parts.length
47
+ when 1
48
+ ["CHEF_SECRET_#{parts[0].upcase}", parts[0]]
49
+ when 2
50
+ [parts[0].upcase, parts[1]]
51
+ else
52
+ raise "Bad secret spec: #{secret}"
53
+ end
54
+
55
+ start.merge({ args: secret_name.split("."),
56
+ env_name: env_name,
57
+ name: secret_name })
58
+ end
59
+
60
+ veil = Veil::CredentialCollection::ChefSecretsFile.from_file(options[:secrets_file])
61
+ packed_data = Hash.new()
62
+
63
+ secrets = options[:secrets].map { |spec| from_secret_spec(spec) }
64
+ secrets += options[:optional_secrets].map { |spec| from_secret_spec(spec, { optional: true }) }
65
+
66
+ secrets.each do |secret|
67
+ veil_args = secret[:args]
68
+
69
+ begin
70
+ secret_value = veil.get(*veil_args)
71
+ rescue
72
+ raise unless secret[:optional]
73
+ next
74
+ end
75
+
76
+ if options[:pack] || options[:use_file]
77
+ STDERR.puts "Packing data using #{veil_args.inspect} and #{secret_value}" if options[:debug]
78
+ if veil_args.length == 2
79
+ packed_data[veil_args[0]] ||= {}
80
+ packed_data[veil_args[0]][veil_args[1]] = secret_value
81
+ elsif veil_args.length == 1
82
+ packed_data[veil_args[0]] = secret_value
83
+ elsif !secret[:optional]
84
+ raise "Invalid secrets name: #{secret[:name]}"
85
+ end
33
86
  else
34
- raise "Bad secret spec: #{secret}"
87
+ STDERR.puts "Setting #{secret[:env_name]}=#{secret_value}" if options[:debug]
88
+ ENV[secret[:env_name]] = secret_value
35
89
  end
36
90
  end
37
91
 
38
- veil = Veil::CredentialCollection::ChefSecretsFile.from_file(options[:secrets_file])
39
- Array(options[:secrets]).each do |secret|
40
- env_name, secret_name = env_name_from_secret_spec(secret)
41
- veil_args = secret_name.split(".")
42
- secret_value = veil.get(*veil_args)
43
- STDERR.puts "Setting #{env_name}=#{secret_value}" if options[:debug]
44
- ENV[env_name] = secret_value
92
+ if options[:pack] && !options[:use_file]
93
+ ENV['CHEF_SECRETS_DATA'] = packed_data.to_json
94
+ end
95
+
96
+ if options[:use_file]
97
+ rd, wd = IO.pipe
98
+ wd.puts packed_data.to_json
99
+ wd.close
100
+ rd.close_on_exec = false
101
+ ENV['CHEF_SECRETS_FD'] = rd.to_i.to_s
45
102
  end
46
103
 
47
- exec(*ARGV)
104
+ exec(*ARGV, close_others: false)
@@ -0,0 +1,16 @@
1
+ module Veil
2
+ class Cipher
3
+ class V1
4
+ def initialize(_opts = {})
5
+ end
6
+
7
+ def encrypt(plaintext)
8
+ raise RuntimeError, "Veil::Cipher::V1#encrypt should never be called"
9
+ end
10
+
11
+ def decrypt(anything)
12
+ anything
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,41 @@
1
+ require "base64"
2
+ require "openssl"
3
+
4
+ module Veil
5
+ class Cipher
6
+ class V2
7
+ attr_reader :key, :iv, :cipher
8
+
9
+ def initialize(opts = {})
10
+ @cipher = OpenSSL::Cipher.new("aes-256-cbc")
11
+ @key = opts[:key] ? Base64.strict_decode64(opts[:key]) : cipher.random_key
12
+ @iv = opts[:iv] ? Base64.strict_decode64(opts[:iv]) : cipher.random_iv
13
+ end
14
+
15
+ def encrypt(plaintext)
16
+ c = cipher
17
+ c.encrypt
18
+ c.key = key
19
+ c.iv = iv
20
+ Base64.strict_encode64(c.update(plaintext) + c.final)
21
+ end
22
+
23
+ def decrypt(ciphertext)
24
+ c = cipher
25
+ c.decrypt
26
+ c.key = key
27
+ c.iv = iv
28
+ JSON.parse(c.update(Base64.strict_decode64(ciphertext)) + c.final, symbolize_names: true)
29
+ end
30
+
31
+ def to_hash
32
+ {
33
+ type: self.class.name,
34
+ key: Base64.strict_encode64(key),
35
+ iv: Base64.strict_encode64(iv)
36
+ }
37
+ end
38
+ alias_method :to_h, :to_hash
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ require "veil/cipher/v1"
2
+ require "veil/cipher/v2"
3
+
4
+ module Veil
5
+ class Cipher
6
+ DEFAULT_DECRYPTOR = Veil::Cipher::V1
7
+ DEFAULT_ENCRYPTOR = Veil::Cipher::V2
8
+
9
+ class << self
10
+ #
11
+ # Create a new Cipher instance
12
+ #
13
+ # Defaults to using v1 for decryption (noop), v2 for encryption.
14
+ # If invoked as default, v2 will generate key and iv.
15
+ #
16
+ # @param opts Hash<Symbol> a hash of options to pass to the constructor
17
+ #
18
+ # @example Veil::Cipher.create(type: "V1")
19
+ # @example Veil::Cipher.create(type: "V2", key: "blah", iv: "vi")
20
+ #
21
+ def create(opts = {})
22
+ case opts
23
+ when {}, nil
24
+ [ DEFAULT_DECRYPTOR.new({}), DEFAULT_ENCRYPTOR.new({}) ]
25
+ else
26
+ cipher = const_get(opts[:type])
27
+ [ cipher.new(opts), cipher.new(opts) ]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,5 +1,6 @@
1
- require "veil/hasher"
1
+ require "veil/cipher"
2
2
  require "veil/credential"
3
+ require "veil/hasher"
3
4
  require "forwardable"
4
5
 
5
6
  module Veil
@@ -13,13 +14,14 @@ module Veil
13
14
 
14
15
  extend Forwardable
15
16
 
16
- attr_reader :credentials, :hasher, :version
17
+ attr_reader :credentials, :hasher, :version, :decryptor, :encryptor
17
18
 
18
19
  def_delegators :@credentials, :size, :length, :find, :map, :select, :each, :[], :keys
19
20
 
20
21
  def initialize(opts = {})
21
22
  @hasher = Veil::Hasher.create(opts[:hasher] || {})
22
- @credentials = expand_credentials_hash(opts[:credentials] || {})
23
+ @decryptor, @encryptor = Veil::Cipher.create(opts[:cipher] || {})
24
+ @credentials = expand_credentials_hash(decryptor.decrypt(opts[:credentials]) || {})
23
25
  @version = opts[:version] || 1
24
26
  end
25
27
 
@@ -28,7 +30,8 @@ module Veil
28
30
  type: self.class.name,
29
31
  version: version,
30
32
  hasher: hasher.to_h,
31
- credentials: credentials_as_hash
33
+ cipher: encryptor.to_h,
34
+ credentials: encryptor.encrypt(credentials_as_hash.to_json)
32
35
  }
33
36
  end
34
37
  alias_method :to_h, :to_hash
@@ -173,6 +176,39 @@ module Veil
173
176
  end
174
177
  end
175
178
 
179
+ def credentials_as_hash
180
+ hash = Hash.new
181
+
182
+ credentials.each do |cred_or_group_name, cred_or_group_attrs|
183
+ if cred_or_group_attrs.is_a?(Hash)
184
+ cred_or_group_attrs.each do |name, cred|
185
+ hash[cred_or_group_name] ||= Hash.new
186
+ hash[cred_or_group_name][name] = cred.to_hash
187
+ end
188
+ else
189
+ hash[cred_or_group_name] = cred_or_group_attrs.to_hash
190
+ end
191
+ end
192
+
193
+ hash
194
+ end
195
+
196
+ def credentials_for_export
197
+ hash = Hash.new
198
+
199
+ credentials.each do |namespace, cred_or_creds|
200
+ if cred_or_creds.is_a?(Veil::Credential)
201
+ hash[namespace] = cred_or_creds.value
202
+ else
203
+ hash[namespace] = {}
204
+ cred_or_creds.each { |name, cred| hash[namespace][name] = cred.value }
205
+ end
206
+ end
207
+
208
+ hash
209
+ end
210
+ alias_method :legacy_credentials_hash, :credentials_for_export
211
+
176
212
  private
177
213
 
178
214
  def add_from_params(params)
@@ -219,21 +255,17 @@ module Veil
219
255
  expanded
220
256
  end
221
257
 
222
- def credentials_as_hash
223
- hash = Hash.new
224
-
225
- credentials.each do |cred_or_group_name, cred_or_group_attrs|
226
- if cred_or_group_attrs.is_a?(Hash)
227
- cred_or_group_attrs.each do |name, cred|
228
- hash[cred_or_group_name] ||= Hash.new
229
- hash[cred_or_group_name][name] = cred.to_hash
230
- end
231
- else
232
- hash[cred_or_group_name] = cred_or_group_attrs.to_hash
258
+ def import_credentials_hash(hash)
259
+ hash.each do |namespace, creds_hash|
260
+ credentials[namespace.to_s] ||= Hash.new
261
+ creds_hash.each do |cred, value|
262
+ credentials[namespace.to_s][cred.to_s] = Veil::Credential.new(
263
+ name: cred.to_s,
264
+ value: value,
265
+ length: value.length
266
+ )
233
267
  end
234
268
  end
235
-
236
- hash
237
269
  end
238
270
  end
239
271
  end
@@ -0,0 +1,43 @@
1
+ require "veil/credential_collection/base"
2
+ require "json"
3
+
4
+ module Veil
5
+ class CredentialCollection
6
+ class ChefSecretsEnv < Base
7
+
8
+ # Create a new ChefSecretsEnv
9
+ #
10
+ # @param [Hash] opts
11
+ # a hash of options to pass to the constructor
12
+ def initialize(opts = {})
13
+ var_name = opts[:var_name] || 'CHEF_SECRETS_DATA'
14
+
15
+ @credentials = {}
16
+ import_credentials_hash(inflate_secrets_from_environment(var_name))
17
+ end
18
+
19
+ # Unsupported methods
20
+ def rotate
21
+ raise NotImplementedError
22
+ end
23
+ alias_method :rotate_credentials, :rotate
24
+ alias_method :save, :rotate
25
+
26
+ def inflate_secrets_from_environment(var_name)
27
+ value = ENV[var_name]
28
+ unless value
29
+ msg = "Env var #{var_name} has not been set. This should by done by "\
30
+ "launching this application via veil-env-wrapper."
31
+ raise InvalidCredentialCollectionEnv.new(msg)
32
+ end
33
+
34
+ begin
35
+ JSON.parse(value)
36
+ rescue JSON::ParserError => e
37
+ msg = "Env var #{var_name} could not be parsed: #{e.message}"
38
+ raise InvalidCredentialCollectionEnv.new(msg)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,57 @@
1
+ require "veil/exceptions"
2
+ require "veil/credential_collection/base"
3
+ require "json"
4
+
5
+ module Veil
6
+ class CredentialCollection
7
+ class ChefSecretsFd < Base
8
+
9
+ # Create a new ChefSecretsFd
10
+ #
11
+ # @param [Hash] opts
12
+ # ignored
13
+ def initialize(opts = {})
14
+ @credentials = {}
15
+ import_credentials_hash(inflate_secrets_from_fd)
16
+ end
17
+
18
+ # Unsupported methods
19
+ def rotate
20
+ raise NotImplementedError
21
+ end
22
+ alias_method :rotate_credentials, :rotate
23
+ alias_method :save, :rotate
24
+
25
+ def inflate_secrets_from_fd
26
+ if ENV['CHEF_SECRETS_FD'].nil?
27
+ raise InvalidCredentialCollectionFd.new("CHEF_SECRETS_FD not found in environment")
28
+ end
29
+
30
+ fd = ENV['CHEF_SECRETS_FD'].to_i
31
+ value = nil
32
+
33
+ begin
34
+ file = IO.new(fd, "r")
35
+ value = file.gets
36
+ rescue StandardError => e
37
+ msg = "A problem occured trying to read passed file descriptor: #{e}"
38
+ raise InvalidCredentialCollectionFd.new(msg)
39
+ ensure
40
+ file.close if file
41
+ end
42
+
43
+ if !value
44
+ msg = "File at CHEF_SECRETS_FD (#{fd}) did not contain any data!"
45
+ raise InvalidCredentialCollectionFd.new(msg)
46
+ end
47
+
48
+ begin
49
+ JSON.parse(value)
50
+ rescue JSON::ParserError => e
51
+ msg = "Chef secrets data could not be parsed: #{e.message}"
52
+ raise InvalidCredentialCollectionFd.new(msg)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -16,7 +16,9 @@ module Veil
16
16
  end
17
17
  end
18
18
 
19
- attr_reader :path, :user, :group
19
+ CURRENT_VERSION = 2.freeze
20
+
21
+ attr_reader :path, :user, :group, :key
20
22
 
21
23
  # Create a new ChefSecretsFile
22
24
  #
@@ -43,10 +45,10 @@ module Veil
43
45
 
44
46
  @user = opts[:user]
45
47
  @group = opts[:group] || @user
46
- @version = opts[:version] || 1
48
+ opts[:version] = CURRENT_VERSION
47
49
  super(opts)
48
50
 
49
- import_legacy_credentials(hash) if import_existing && legacy
51
+ import_credentials_hash(hash) if import_existing && legacy
50
52
  end
51
53
 
52
54
  # Set the secrets file path
@@ -57,11 +59,17 @@ module Veil
57
59
  @path = File.expand_path(path)
58
60
  end
59
61
 
60
- # Save the CredentialCollection to file
62
+ # Save the CredentialCollection to file, encrypt it
61
63
  def save
62
- FileUtils.mkdir_p(File.dirname(path)) unless File.directory?(File.dirname(path))
64
+ FileUtils.mkdir_p(File.dirname(path))
63
65
 
64
66
  f = Tempfile.new("veil") # defaults to mode 0600
67
+
68
+ if existing
69
+ @user ||= existing.uid
70
+ @group ||= existing.gid
71
+ end
72
+
65
73
  FileUtils.chown(user, group, f.path) if user
66
74
  f.puts(JSON.pretty_generate(secrets_hash))
67
75
  f.flush
@@ -73,32 +81,13 @@ module Veil
73
81
 
74
82
  # Return the instance as a secrets style hash
75
83
  def secrets_hash
76
- { "veil" => to_h }.merge(legacy_credentials_hash)
84
+ { "veil" => to_h }
77
85
  end
78
86
 
79
- # Return the credentials in a legacy chef secrets hash
80
- def legacy_credentials_hash
81
- hash = Hash.new
82
-
83
- to_h[:credentials].each do |namespace, creds|
84
- hash[namespace] = {}
85
- creds.each { |name, cred| hash[namespace][name] = cred[:value] }
86
- end
87
-
88
- hash
89
- end
90
-
91
- def import_legacy_credentials(hash)
92
- hash.each do |namespace, creds_hash|
93
- credentials[namespace.to_s] ||= Hash.new
94
- creds_hash.each do |cred, value|
95
- credentials[namespace.to_s][cred.to_s] = Veil::Credential.new(
96
- name: cred.to_s,
97
- value: value,
98
- length: value.length
99
- )
100
- end
101
- end
87
+ def existing
88
+ @existing ||= File.stat(path)
89
+ rescue Errno::ENOENT
90
+ nil
102
91
  end
103
92
  end
104
93
  end
@@ -1,5 +1,7 @@
1
1
  require "veil/credential_collection/base"
2
+ require "veil/credential_collection/chef_secrets_fd"
2
3
  require "veil/credential_collection/chef_secrets_file"
4
+ require "veil/credential_collection/chef_secrets_env"
3
5
 
4
6
  module Veil
5
7
  class CredentialCollection
@@ -8,6 +10,10 @@ module Veil
8
10
  klass = case opts[:provider]
9
11
  when 'chef-secrets-file'
10
12
  ChefSecretsFile
13
+ when 'chef-secrets-env'
14
+ ChefSecretsEnv
15
+ when 'chef-secrets-fd'
16
+ ChefSecretsFd
11
17
  else
12
18
  raise UnknownProvider, "Unknown provider: #{opts[:provider]}"
13
19
  end
@@ -3,7 +3,10 @@ module Veil
3
3
  class InvalidSecret < StandardError; end
4
4
  class InvalidParameter < StandardError; end
5
5
  class InvalidHasher < StandardError; end
6
- class InvalidCredentialCollectionFile < StandardError; end
6
+ class InvalidCredentialCollection < StandardError; end
7
+ class InvalidCredentialCollectionFile < InvalidCredentialCollection; end
8
+ class InvalidCredentialCollectionEnv < InvalidCredentialCollection; end
9
+ class InvalidCredentialCollectionFd < InvalidCredentialCollection; end
7
10
  class MissingParameter < StandardError; end
8
11
  class NotImplmented < StandardError; end
9
12
  class InvalidCredentialHash < StandardError; end
data/lib/veil/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Veil
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.9"
3
3
  end
@@ -0,0 +1,15 @@
1
+ require "spec_helper"
2
+
3
+ describe Veil::Cipher::V1 do
4
+ describe "#decrypt" do
5
+ it "does nothing" do
6
+ expect(described_class.new().decrypt("hi")).to eq("hi")
7
+ end
8
+ end
9
+
10
+ describe "#encrypt" do
11
+ it "raises a RuntimeError" do
12
+ expect { described_class.new().encrypt("hi") }.to raise_error(RuntimeError)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ require "spec_helper"
2
+ require "openssl"
3
+
4
+ describe Veil::Cipher::V2 do
5
+ let(:iv64) { Base64.strict_encode64("mondaytue16bytes") }
6
+ let(:key64) { Base64.strict_encode64("thursdayfridaysaturdaysun32bytes") }
7
+ let(:ciphertext64) { "qS6SRmgOgSta2jSdD60sJmu83cRgy4DUJ2nKZwStrqs=" }
8
+ let(:plainhash) { { "chef-server": "test-value" } }
9
+
10
+ describe "#new" do
11
+ it "accepts passed key and iv base64-encoded data" do
12
+ cipher = described_class.new(iv: iv64, key: key64)
13
+
14
+ expect(cipher.iv).to eq("mondaytue16bytes")
15
+ expect(cipher.key).to eq("thursdayfridaysaturdaysun32bytes")
16
+ end
17
+
18
+ it "generates key and iv data if none was passed" do
19
+ openssl = double(OpenSSL::Cipher)
20
+ expect(OpenSSL::Cipher).to receive(:new).and_return(openssl)
21
+ expect(openssl).to receive(:random_iv).and_return("random iv")
22
+ expect(openssl).to receive(:random_key).and_return("random key")
23
+ cipher = described_class.new()
24
+
25
+ expect(cipher.iv).to eq("random iv")
26
+ expect(cipher.key).to eq("random key")
27
+ end
28
+ end
29
+
30
+ describe "#decrypt" do
31
+ it "parses decrypted json data" do
32
+ expect(described_class.new(iv: iv64, key: key64).decrypt(ciphertext64)).to eq(plainhash)
33
+ end
34
+ end
35
+
36
+ describe "#to_hash" do
37
+ it "base64-encodes iv and key" do
38
+ expected = {
39
+ iv: "bW9uZGF5dHVlMTZieXRlcw==",
40
+ key: "dGh1cnNkYXlmcmlkYXlzYXR1cmRheXN1bjMyYnl0ZXM=",
41
+ type: "Veil::Cipher::V2"
42
+ }
43
+ expect(described_class.new(iv: iv64, key: key64).to_hash).to eq(expected)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ describe Veil::Cipher do
4
+ describe "#self.create" do
5
+ it "defaults to noop decrypt, v2 encrypt" do
6
+ dec, enc = described_class.create()
7
+ expect(dec.class).to eq(Veil::Cipher::V1)
8
+ expect(enc.class).to eq(Veil::Cipher::V2)
9
+ end
10
+
11
+ it "parses the non-namespaced type" do
12
+ dec, enc = described_class.create(type: "V2")
13
+ expect(dec.class).to eq(Veil::Cipher::V2)
14
+ expect(enc.class).to eq(Veil::Cipher::V2)
15
+ end
16
+
17
+ it "parses the complete type" do
18
+ dec, enc = described_class.create(type: "Veil::Cipher::V2")
19
+ expect(dec.class).to eq(Veil::Cipher::V2)
20
+ expect(enc.class).to eq(Veil::Cipher::V2)
21
+ end
22
+ end
23
+ end
@@ -25,10 +25,11 @@ describe Veil::CredentialCollection::Base do
25
25
  context "with credential options" do
26
26
  it "builds the credentials" do
27
27
  subject.add("foo", "bar", length: 22)
28
- creds_hash = subject.to_hash[:credentials]
28
+ creds_hash = subject["foo"]["bar"].to_hash
29
+ new_instance = described_class.new(credentials: { foo: { bar: creds_hash } })
29
30
 
30
- new_instance = described_class.new(credentials: creds_hash)
31
- expect(new_instance["foo"]["bar"].value).to eq(subject["foo"]["bar"].value)
31
+ expected = subject["foo"]["bar"].value
32
+ expect(new_instance["foo"]["bar"].value).to eq(expected)
32
33
  end
33
34
  end
34
35
 
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe Veil::CredentialCollection::ChefSecretsEnv do
4
+ describe "#new" do
5
+ context "env variable is set" do
6
+ let(:var_name) { "CHEF_SECRETS_DATA" }
7
+
8
+ before(:each) do
9
+ ENV[var_name] = '{ "secret_service": { "secret_name": "secret_value" } }'
10
+ end
11
+
12
+ it 'reads the secret from the env var CHEF_SECRETS_DATA' do
13
+ expect(subject.get("secret_service", "secret_name")).to eq("secret_value")
14
+ end
15
+
16
+ context "env variable name is passed" do
17
+ let(:var_name) { "CHEF_SECRETS_DATA_2" }
18
+ let(:subject) { described_class.new(var_name: var_name) }
19
+
20
+ it 'reads the secret from the passed env var name' do
21
+ expect(subject.get("secret_service", "secret_name")).to eq("secret_value")
22
+ end
23
+ end
24
+
25
+ context "env var content cannot be parsed" do
26
+ before(:each) do
27
+ ENV[var_name] = '{ "secre '
28
+ end
29
+
30
+ it "re-raises the JSON parse error" do
31
+ expect{ subject.get("secret_service", "secret_name") }.to raise_error(Veil::InvalidCredentialCollectionEnv)
32
+ end
33
+ end
34
+ end
35
+
36
+ context "env variable is not set" do
37
+ let(:var_name) { "CHEF_SECRETS_DATA" }
38
+
39
+ before(:each) do
40
+ ENV.delete(var_name)
41
+ end
42
+
43
+ it 'raises an exception' do
44
+ expect{ described_class.new }.to raise_error(Veil::InvalidCredentialCollectionEnv)
45
+ end
46
+ end
47
+
48
+ context "unsupported methods" do
49
+ let(:var_name) { "CHEF_SECRETS_DATA" }
50
+
51
+ before(:each) do
52
+ ENV[var_name] = '{}'
53
+ end
54
+
55
+ it 'does not support #rotate' do
56
+ expect{ subject.rotate }.to raise_error(NotImplementedError)
57
+ end
58
+
59
+ it 'does not support #save' do
60
+ expect{ subject.save }.to raise_error(NotImplementedError)
61
+ end
62
+
63
+ it 'does not support #rotate_hasher' do
64
+ expect{ subject.rotate_hasher }.to raise_error(NotImplementedError)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe Veil::CredentialCollection::ChefSecretsFd do
4
+ describe "#new" do
5
+ let(:content) { '{ "secret_service": { "secret_name": "secret_value" } }' }
6
+ let(:content_file) {
7
+ rd, wr = IO.pipe
8
+ wr.puts content
9
+ wr.close
10
+ rd
11
+ }
12
+
13
+ context "env variable is set" do
14
+ before(:each) do
15
+ ENV['CHEF_SECRETS_FD'] = content_file.to_i.to_s
16
+ end
17
+
18
+ it 'reads the secret from the passed file descriptor' do
19
+ expect(subject.get("secret_service", "secret_name")).to eq("secret_value")
20
+ end
21
+
22
+ # TODO(ssd) 2017-03-22: We wanted a test for closing the FD, but
23
+ # didn't want to fight with ruby today.
24
+
25
+ context "env var content cannot be parsed" do
26
+ let(:content) { '{ "secre ' }
27
+
28
+ it "re-raises the JSON parse error" do
29
+ expect{ subject.get("secret_service", "secret_name") }.to raise_error(Veil::InvalidCredentialCollectionFd)
30
+ end
31
+ end
32
+ end
33
+
34
+ context "CHEF_SECRETS_FD is not set" do
35
+ before(:each) do
36
+ ENV.delete('CHEF_SECRETS_FD')
37
+ end
38
+
39
+ it 'raises an exception' do
40
+ expect{ described_class.new }.to raise_error(Veil::InvalidCredentialCollectionFd)
41
+ end
42
+ end
43
+
44
+ context "unsupported methods" do
45
+ let(:content) { '{}' }
46
+
47
+ before(:each) do
48
+ ENV['CHEF_SECRETS_FD'] = content_file.to_i.to_s
49
+ end
50
+
51
+ it 'does not support #rotate' do
52
+ expect{ subject.rotate }.to raise_error(NotImplementedError)
53
+ end
54
+
55
+ it 'does not support #save' do
56
+ expect{ subject.save }.to raise_error(NotImplementedError)
57
+ end
58
+
59
+ it 'does not support #rotate_hasher' do
60
+ expect{ subject.rotate_hasher }.to raise_error(NotImplementedError)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -7,10 +7,12 @@ describe Veil::CredentialCollection::ChefSecretsFile do
7
7
  let!(:file) { Tempfile.new("private_chef_secrets.json") }
8
8
  let(:user) { "opscode_user" }
9
9
  let(:group) { "opscode_group" }
10
+ let(:version) { 1 }
10
11
  let(:content) do
11
12
  {
12
13
  "veil" => {
13
14
  "type" => "Veil::CredentialCollection::ChefSecretsFile",
15
+ "version" => version,
14
16
  "hasher" => {},
15
17
  "credentials" => {}
16
18
  }
@@ -66,7 +68,7 @@ describe Veil::CredentialCollection::ChefSecretsFile do
66
68
  end
67
69
 
68
70
  describe "#save" do
69
- it "saves the content to a machine loadable file" do
71
+ it "saves the content to a file it can read" do
70
72
  file.rewind
71
73
  creds = described_class.new(path: file.path)
72
74
  creds.add("redis_lb", "password")
@@ -80,6 +82,14 @@ describe Veil::CredentialCollection::ChefSecretsFile do
80
82
  expect(new_creds["postgresql"]["sql_ro_password"].value).to eq(creds["postgresql"]["sql_ro_password"].value)
81
83
  end
82
84
 
85
+ it "saves the content in an encrypted form" do
86
+ creds = described_class.new(path: file.path)
87
+ creds.add("postgresql", "sql_ro_password", value: "kneipenpathos")
88
+ creds.save
89
+
90
+ expect(IO.read(file.path)).to_not match(/kneipenpathos/)
91
+ end
92
+
83
93
  context "when using ownership management" do
84
94
  let(:tmpfile) do
85
95
  s = StringIO.new
@@ -87,56 +97,170 @@ describe Veil::CredentialCollection::ChefSecretsFile do
87
97
  s
88
98
  end
89
99
 
90
- context "when the user is set" do
91
- it "gives the file proper permissions" do
92
- expect(Tempfile).to receive(:new).with("veil").and_return(tmpfile)
93
- expect(FileUtils).to receive(:chown).with(user, user, "/tmp/unguessable")
94
- expect(FileUtils).to receive(:mv).with("/tmp/unguessable", file.path)
100
+ context "when the target file does not exist" do
101
+
102
+ let(:secrets_file) { double(File) }
103
+ before(:each) do
104
+ allow(File).to receive(:stat).with(file.path).and_raise(Errno::ENOENT)
105
+ end
106
+
107
+ context "when the user is not set" do
108
+ it "does not change any permissions" do
109
+ expect(Tempfile).to receive(:new).with("veil").and_return(tmpfile)
110
+ expect(FileUtils).not_to receive(:chown)
111
+ expect(FileUtils).to receive(:mv).with("/tmp/unguessable", file.path)
112
+
113
+ creds = described_class.new(path: file.path)
114
+ creds.add("redis_lb", "password")
115
+ creds.save
116
+ end
117
+ end
118
+
119
+ context "when the user is set" do
120
+ it "gives the file proper permissions" do
121
+ expect(Tempfile).to receive(:new).with("veil").and_return(tmpfile)
122
+ expect(FileUtils).to receive(:chown).with(user, user, "/tmp/unguessable")
123
+ expect(FileUtils).to receive(:mv).with("/tmp/unguessable", file.path)
95
124
 
96
- creds = described_class.new(path: file.path,
97
- user: user)
98
- creds.add("redis_lb", "password")
99
- creds.save
125
+ creds = described_class.new(path: file.path,
126
+ user: user)
127
+ creds.add("redis_lb", "password")
128
+ creds.save
129
+ end
100
130
  end
101
131
  end
102
132
 
103
- context "when user and group are set" do
104
- it "gives the file proper permissions" do
105
- expect(Tempfile).to receive(:new).with("veil").and_return(tmpfile)
106
- expect(FileUtils).to receive(:chown).with(user, group, "/tmp/unguessable")
107
- expect(FileUtils).to receive(:mv).with("/tmp/unguessable", file.path)
108
-
109
- creds = described_class.new(path: file.path,
110
- user: user,
111
- group: group)
112
- creds.add("redis_lb", "password")
113
- creds.save
133
+ context "when the target file exists" do
134
+
135
+ let(:secrets_file) { double(File) }
136
+ let(:secrets_file_stat) { double(File, uid: 100, gid: 1000) }
137
+ before(:each) do
138
+ allow(File).to receive(:stat).with(file.path).and_return(secrets_file_stat)
139
+ end
140
+
141
+ context "when the user is not set" do
142
+ it "keeps the existing file permissions" do
143
+ expect(Tempfile).to receive(:new).with("veil").and_return(tmpfile)
144
+ expect(FileUtils).to receive(:chown).with(100, 1000, "/tmp/unguessable")
145
+ expect(FileUtils).to receive(:mv).with("/tmp/unguessable", file.path)
146
+
147
+ creds = described_class.new(path: file.path)
148
+ creds.add("redis_lb", "password")
149
+ creds.save
150
+ end
151
+ end
152
+
153
+ context "when the user is set" do
154
+ it "gives the file proper permissions" do
155
+ expect(Tempfile).to receive(:new).with("veil").and_return(tmpfile)
156
+ expect(FileUtils).to receive(:chown).with(user, user, "/tmp/unguessable")
157
+ expect(FileUtils).to receive(:mv).with("/tmp/unguessable", file.path)
158
+
159
+ creds = described_class.new(path: file.path,
160
+ user: user)
161
+ creds.add("redis_lb", "password")
162
+ creds.save
163
+ end
114
164
  end
115
165
 
116
- it "gives the file proper permission even when called from_file" do
117
- file.puts("{}"); file.rewind
118
- expect(Tempfile).to receive(:new).with("veil").and_return(tmpfile)
119
- expect(FileUtils).to receive(:chown).with(user, group, "/tmp/unguessable")
120
- expect(FileUtils).to receive(:mv).with("/tmp/unguessable", file.path)
121
-
122
- creds = described_class.from_file(file.path,
123
- user: user,
124
- group: group)
125
- creds.add("redis_lb", "password")
126
- creds.save
166
+ context "when user and group are set" do
167
+ it "gives the file proper permissions" do
168
+ expect(Tempfile).to receive(:new).with("veil").and_return(tmpfile)
169
+ expect(FileUtils).to receive(:chown).with(user, group, "/tmp/unguessable")
170
+ expect(FileUtils).to receive(:mv).with("/tmp/unguessable", file.path)
171
+
172
+ creds = described_class.new(path: file.path,
173
+ user: user,
174
+ group: group)
175
+ creds.add("redis_lb", "password")
176
+ creds.save
177
+ end
178
+
179
+ it "gives the file proper permission even when called from_file" do
180
+ file.puts("{}"); file.rewind
181
+ expect(Tempfile).to receive(:new).with("veil").and_return(tmpfile)
182
+ expect(FileUtils).to receive(:chown).with(user, group, "/tmp/unguessable")
183
+ expect(FileUtils).to receive(:mv).with("/tmp/unguessable", file.path)
184
+
185
+ creds = described_class.from_file(file.path,
186
+ user: user,
187
+ group: group)
188
+ creds.add("redis_lb", "password")
189
+ creds.save
190
+ end
127
191
  end
128
192
  end
129
193
  end
130
194
 
131
- it "saves the version number" do
195
+ it "saves the latest version number" do
132
196
  allow(FileUtils).to receive(:chown)
133
197
  file.rewind
134
- creds = described_class.new(path: file.path, version: 12)
198
+ creds = described_class.new(path: file.path)
135
199
  creds.save
136
200
 
137
201
  file.rewind
138
202
  new_creds = described_class.from_file(file.path)
139
- expect(new_creds.version).to eq(12)
203
+ expect(new_creds.version).to eq(2)
204
+ end
205
+
206
+ context "reading an unencrypted file" do
207
+ let(:existing_content) { <<EOF
208
+ {
209
+ "veil": {
210
+ "type": "Veil::CredentialCollection::ChefSecretsFile",
211
+ "version": 1,
212
+ "hasher": {
213
+ "type": "Veil::Hasher::PBKDF2",
214
+ "secret": "5f6e76ae7f5b631a96f5142b2a4b837e23ab6d204dc9e635fc18783ae8d253596f9cd9b65e295c96cee0b9cd1171cae1ca2e897ca5419106785d99d4a42fe9897f2fc7f537a05d39f19b26aa05500e93d65621ed3cdbb718d3005f808fe04a2e2267c43f5100dfe1a6ab3b6462d7fe57bc3adced263fd8c2c55ddebc2f9067b475868e9fb950bf2ae37889e22c42fd686b168b25410a4fa7689a32686fab76e57cd64482ff411be69a4c9abefc5ed64227fb42db05f3b221ddc18c6bb7ca54625cd8a5426be0d9c2161f09f35a04bd9b07884f4319389801c123d0bd345d4218434d9aee87dbb115c5cc22d1c87ffc447c2a008e89edda7e25453076dd478df0f08d50206724cd055741b7521b889fbf6d12af7946ee069bdb60cf79b14e2dad910bab97ae0ff6a9c1f88556e493df26bff007728317683a14433b30a6b6537095c1a8906a08d8a067a1b2a0443ad6fcf007e6bd70c8a3bbfe571044652d3960fd200d23f046b2493463692b570f1c94dabb05933e8d6bcd7e59a708ac8b385034a32b5b1f20635cb10e8027376f496ee1e8496c3517e24bfab3a840f0994a7fde433dc942be781488e6ebbbc68872467ebd216c71e092b2adae600a50bc2ace312cc8cd7949ddb16b72555c9d511211e22383eb515dad22032e80c11f3193a1b357f1c073e4770e314a960bbeada64b36078cfc18e806c03743a0dcd1f77ef3",
215
+ "salt": "78a8140351ecbfaee137655377aa15138655dc65a49c53ca5f6ee05c7b9c3db9dbc0e2bfdf14af3a062a1cc513e4e086126cf428890df3caf11d9714729027ac93fbf8b7bd9d8fc734b7b4a84c979b45e804e573f93f5cd6dab0b1cf7e5b6191c0924e61b84e8289065918fa991846e1a0f19f357c497c9fa4c420fda0578c14",
216
+ "iterations": 10000,
217
+ "hash_function": "OpenSSL::Digest::SHA512"
218
+ },
219
+ "credentials": {
220
+ "postgresql": {
221
+ "db_superuser_password": {
222
+ "type": "Veil::Credential",
223
+ "name": "db_superuser_password",
224
+ "group": "postgresql",
225
+ "value": "37f5c43dd8bab089821e5a49fe0ac17128c6d1878fe632e687889eaaea1980b51b3e3df755d2610cffe6896c1df9f23bdda3",
226
+ "version": 1,
227
+ "length": 100,
228
+ "frozen": false
229
+ }
230
+ }
231
+ }
232
+ },
233
+ "postgresql": {
234
+ "db_superuser_password": "37f5c43dd8bab089821e5a49fe0ac17128c6d1878fe632e687889eaaea1980b51b3e3df755d2610cffe6896c1df9f23bdda3"
235
+ }
236
+ }
237
+ EOF
238
+ }
239
+ let(:tempfile) { Tempfile.new("private-chef-secrets").path }
240
+
241
+ before(:each) do
242
+ File.write(tempfile, existing_content)
243
+ end
244
+
245
+ it "reads the secrets" do
246
+ creds = described_class.new(path: tempfile)
247
+ expect(creds["postgresql"]["db_superuser_password"].value).to eq("37f5c43dd8bab089821e5a49fe0ac17128c6d1878fe632e687889eaaea1980b51b3e3df755d2610cffe6896c1df9f23bdda3")
248
+ end
249
+
250
+ it "saves the file encrypted" do
251
+ creds = described_class.new(path: tempfile)
252
+ creds.save
253
+
254
+ expect(IO.read(tempfile)).to_not match("37f5c43dd8bab089821e5a49fe0ac17128c6d1878fe632e687889eaaea1980b51b3e3df755d2610cffe6896c1df9f23bdda3")
255
+ end
256
+
257
+ it "keeps only the veil key of the file's hash" do
258
+ creds = described_class.new(path: tempfile)
259
+ creds.save
260
+
261
+ json = JSON.parse(IO.read(tempfile))
262
+ expect(json.keys).to eq(["veil"])
263
+ end
140
264
  end
141
265
  end
142
266
  end
@@ -11,6 +11,15 @@ describe Veil::CredentialCollection do
11
11
  end
12
12
  end
13
13
 
14
+ context 'passing provider "chef-secrets-env"' do
15
+ let(:opts) { { provider: 'chef-secrets-env' } }
16
+
17
+ it 'instantiates ChefSecretsFile with all options' do
18
+ expect(Veil::CredentialCollection::ChefSecretsEnv).to receive(:new).with(opts)
19
+ described_class.from_config(opts)
20
+ end
21
+ end
22
+
14
23
  context 'passing anything else as provider' do
15
24
  let(:opts) { { provider: 'vault' } }
16
25
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: veil
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef Software, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-13 00:00:00.000000000 Z
11
+ date: 2021-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: rake
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -82,10 +68,9 @@ dependencies:
82
68
  version: '3.0'
83
69
  description: Veil is a Ruby Gem for generating secure secrets from a shared secret
84
70
  email:
85
- - partnereng@chef.io
71
+ - info@chef.io
86
72
  executables:
87
- - console
88
- - setup
73
+ - veil-dump-secrets
89
74
  - veil-env-helper
90
75
  - veil-ingest-secret
91
76
  extensions: []
@@ -94,12 +79,18 @@ files:
94
79
  - LICENSE
95
80
  - bin/console
96
81
  - bin/setup
82
+ - bin/veil-dump-secrets
97
83
  - bin/veil-env-helper
98
84
  - bin/veil-ingest-secret
99
85
  - lib/veil.rb
86
+ - lib/veil/cipher.rb
87
+ - lib/veil/cipher/v1.rb
88
+ - lib/veil/cipher/v2.rb
100
89
  - lib/veil/credential.rb
101
90
  - lib/veil/credential_collection.rb
102
91
  - lib/veil/credential_collection/base.rb
92
+ - lib/veil/credential_collection/chef_secrets_env.rb
93
+ - lib/veil/credential_collection/chef_secrets_fd.rb
103
94
  - lib/veil/credential_collection/chef_secrets_file.rb
104
95
  - lib/veil/exceptions.rb
105
96
  - lib/veil/hasher.rb
@@ -108,7 +99,12 @@ files:
108
99
  - lib/veil/hasher/pbkdf2.rb
109
100
  - lib/veil/utils.rb
110
101
  - lib/veil/version.rb
102
+ - spec/cipher/v1_spec.rb
103
+ - spec/cipher/v2_spec.rb
104
+ - spec/cipher_spec.rb
111
105
  - spec/credential_collection/base_spec.rb
106
+ - spec/credential_collection/chef_secrets_env_spec.rb
107
+ - spec/credential_collection/chef_secrets_fd_spec.rb
112
108
  - spec/credential_collection/chef_secrets_file_spec.rb
113
109
  - spec/credential_collection_spec.rb
114
110
  - spec/credential_spec.rb
@@ -119,7 +115,7 @@ files:
119
115
  - spec/spec_helper.rb
120
116
  - spec/utils_spec.rb
121
117
  - spec/veil_spec.rb
122
- homepage: https://github.com/chef/chef-server/
118
+ homepage: https://github.com/chef/chef_secrets/
123
119
  licenses:
124
120
  - Apache-2.0
125
121
  metadata: {}
@@ -138,13 +134,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
134
  - !ruby/object:Gem::Version
139
135
  version: '0'
140
136
  requirements: []
141
- rubyforge_project:
142
- rubygems_version: 2.6.10
137
+ rubygems_version: 3.1.4
143
138
  signing_key:
144
139
  specification_version: 4
145
140
  summary: Veil is a Ruby Gem for generating secure secrets from a shared secret
146
141
  test_files:
142
+ - spec/cipher/v1_spec.rb
143
+ - spec/cipher/v2_spec.rb
144
+ - spec/cipher_spec.rb
147
145
  - spec/credential_collection/base_spec.rb
146
+ - spec/credential_collection/chef_secrets_env_spec.rb
147
+ - spec/credential_collection/chef_secrets_fd_spec.rb
148
148
  - spec/credential_collection/chef_secrets_file_spec.rb
149
149
  - spec/credential_collection_spec.rb
150
150
  - spec/credential_spec.rb