veil 0.2.0 → 0.3.9

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 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