veil 0.2.0 → 0.3.0

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
2
  SHA1:
3
- metadata.gz: cfc065388eeda8de97ced25becd31b968eaebd1c
4
- data.tar.gz: 4f254696b35ccd7924bb84db6a18b65629570533
3
+ metadata.gz: 6101318787572fead55a5c0c06c46227f840114d
4
+ data.tar.gz: af7f78cc9004e614ba49757eda9cba8996646f8d
5
5
  SHA512:
6
- metadata.gz: 858dd8cba868d10bc3559b7ea6e4467a6cbeb9a3316244c0289b87adeef13709306a9c909d8dd8b1a1593b54a3541dd7cda1b3dce6ece469c937db2e83990270
7
- data.tar.gz: 831847b81b49f810c5b37e4f7a4211723f9fa84bcf5a26cce62a76ef9861d7288cd31bcfea08992aaee068b001c157ed1757e78311487f98d330cdc68d5fdea7
6
+ metadata.gz: 5f50e2f7e67dce633ec09ca5918e37949ed0e073499ac0878050ad40ed598f5a455ed01e635d8fb0bb13238e785b5204ec1ae4d9770bb73bd5067baf8c19c68c
7
+ data.tar.gz: 241a21af2f050f4c193b6389a9f69673b1f913f24755cb54f7750ad83366cf363dc301278057faf9ed181489ffaa4c698a65684e197f8a1bcd9964470361c9a5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,7 +45,7 @@ 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
51
  import_legacy_credentials(hash) if import_existing && legacy
@@ -57,9 +59,9 @@ 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
65
67
  FileUtils.chown(user, group, f.path) if user
@@ -73,20 +75,24 @@ module Veil
73
75
 
74
76
  # Return the instance as a secrets style hash
75
77
  def secrets_hash
76
- { "veil" => to_h }.merge(legacy_credentials_hash)
78
+ { "veil" => to_h }
77
79
  end
78
80
 
79
- # Return the credentials in a legacy chef secrets hash
80
- def legacy_credentials_hash
81
+ def credentials_for_export
81
82
  hash = Hash.new
82
83
 
83
- to_h[:credentials].each do |namespace, creds|
84
- hash[namespace] = {}
85
- creds.each { |name, cred| hash[namespace][name] = cred[:value] }
84
+ credentials.each do |namespace, cred_or_creds|
85
+ if cred_or_creds.is_a?(Veil::Credential)
86
+ hash[namespace] = cred_or_creds.value
87
+ else
88
+ hash[namespace] = {}
89
+ cred_or_creds.each { |name, cred| hash[namespace][name] = cred.value }
90
+ end
86
91
  end
87
92
 
88
93
  hash
89
94
  end
95
+ alias_method :legacy_credentials_hash, :credentials_for_export
90
96
 
91
97
  def import_legacy_credentials(hash)
92
98
  hash.each do |namespace, creds_hash|
@@ -1,3 +1,3 @@
1
1
  module Veil
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
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("mondaytuesdaywednesday") }
6
+ let(:key64) { Base64.strict_encode64("thursdayfridaysaturdaysundaymonday") }
7
+ let(:ciphertext64) { "yhHR8ZVUfzRv+4dvcUFlitdmCS3ybi2dGLofzEF1Ibw=" }
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("mondaytuesdaywednesday")
15
+ expect(cipher.key).to eq("thursdayfridaysaturdaysundaymonday")
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: "bW9uZGF5dHVlc2RheXdlZG5lc2RheQ==",
40
+ key: "dGh1cnNkYXlmcmlkYXlzYXR1cmRheXN1bmRheW1vbmRheQ==",
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
 
@@ -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
@@ -128,15 +138,75 @@ describe Veil::CredentialCollection::ChefSecretsFile do
128
138
  end
129
139
  end
130
140
 
131
- it "saves the version number" do
141
+ it "saves the latest version number" do
132
142
  allow(FileUtils).to receive(:chown)
133
143
  file.rewind
134
- creds = described_class.new(path: file.path, version: 12)
144
+ creds = described_class.new(path: file.path)
135
145
  creds.save
136
146
 
137
147
  file.rewind
138
148
  new_creds = described_class.from_file(file.path)
139
- expect(new_creds.version).to eq(12)
149
+ expect(new_creds.version).to eq(2)
150
+ end
151
+
152
+ context "reading an unencrypted file" do
153
+ let(:existing_content) { <<EOF
154
+ {
155
+ "veil": {
156
+ "type": "Veil::CredentialCollection::ChefSecretsFile",
157
+ "version": 1,
158
+ "hasher": {
159
+ "type": "Veil::Hasher::PBKDF2",
160
+ "secret": "5f6e76ae7f5b631a96f5142b2a4b837e23ab6d204dc9e635fc18783ae8d253596f9cd9b65e295c96cee0b9cd1171cae1ca2e897ca5419106785d99d4a42fe9897f2fc7f537a05d39f19b26aa05500e93d65621ed3cdbb718d3005f808fe04a2e2267c43f5100dfe1a6ab3b6462d7fe57bc3adced263fd8c2c55ddebc2f9067b475868e9fb950bf2ae37889e22c42fd686b168b25410a4fa7689a32686fab76e57cd64482ff411be69a4c9abefc5ed64227fb42db05f3b221ddc18c6bb7ca54625cd8a5426be0d9c2161f09f35a04bd9b07884f4319389801c123d0bd345d4218434d9aee87dbb115c5cc22d1c87ffc447c2a008e89edda7e25453076dd478df0f08d50206724cd055741b7521b889fbf6d12af7946ee069bdb60cf79b14e2dad910bab97ae0ff6a9c1f88556e493df26bff007728317683a14433b30a6b6537095c1a8906a08d8a067a1b2a0443ad6fcf007e6bd70c8a3bbfe571044652d3960fd200d23f046b2493463692b570f1c94dabb05933e8d6bcd7e59a708ac8b385034a32b5b1f20635cb10e8027376f496ee1e8496c3517e24bfab3a840f0994a7fde433dc942be781488e6ebbbc68872467ebd216c71e092b2adae600a50bc2ace312cc8cd7949ddb16b72555c9d511211e22383eb515dad22032e80c11f3193a1b357f1c073e4770e314a960bbeada64b36078cfc18e806c03743a0dcd1f77ef3",
161
+ "salt": "78a8140351ecbfaee137655377aa15138655dc65a49c53ca5f6ee05c7b9c3db9dbc0e2bfdf14af3a062a1cc513e4e086126cf428890df3caf11d9714729027ac93fbf8b7bd9d8fc734b7b4a84c979b45e804e573f93f5cd6dab0b1cf7e5b6191c0924e61b84e8289065918fa991846e1a0f19f357c497c9fa4c420fda0578c14",
162
+ "iterations": 10000,
163
+ "hash_function": "OpenSSL::Digest::SHA512"
164
+ },
165
+ "credentials": {
166
+ "postgresql": {
167
+ "db_superuser_password": {
168
+ "type": "Veil::Credential",
169
+ "name": "db_superuser_password",
170
+ "group": "postgresql",
171
+ "value": "37f5c43dd8bab089821e5a49fe0ac17128c6d1878fe632e687889eaaea1980b51b3e3df755d2610cffe6896c1df9f23bdda3",
172
+ "version": 1,
173
+ "length": 100,
174
+ "frozen": false
175
+ }
176
+ }
177
+ }
178
+ },
179
+ "postgresql": {
180
+ "db_superuser_password": "37f5c43dd8bab089821e5a49fe0ac17128c6d1878fe632e687889eaaea1980b51b3e3df755d2610cffe6896c1df9f23bdda3"
181
+ }
182
+ }
183
+ EOF
184
+ }
185
+ let(:tempfile) { Tempfile.new("private-chef-secrets").path }
186
+
187
+ before(:each) do
188
+ File.write(tempfile, existing_content)
189
+ end
190
+
191
+ it "reads the secrets" do
192
+ creds = described_class.new(path: tempfile)
193
+ expect(creds["postgresql"]["db_superuser_password"].value).to eq("37f5c43dd8bab089821e5a49fe0ac17128c6d1878fe632e687889eaaea1980b51b3e3df755d2610cffe6896c1df9f23bdda3")
194
+ end
195
+
196
+ it "saves the file encrypted" do
197
+ creds = described_class.new(path: tempfile)
198
+ creds.save
199
+
200
+ expect(IO.read(tempfile)).to_not match("37f5c43dd8bab089821e5a49fe0ac17128c6d1878fe632e687889eaaea1980b51b3e3df755d2610cffe6896c1df9f23bdda3")
201
+ end
202
+
203
+ it "keeps only the veil key of the file's hash" do
204
+ creds = described_class.new(path: tempfile)
205
+ creds.save
206
+
207
+ json = JSON.parse(IO.read(tempfile))
208
+ expect(json.keys).to eq(["veil"])
209
+ end
140
210
  end
141
211
  end
142
212
  end
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.0
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: 2017-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt
@@ -86,6 +86,7 @@ email:
86
86
  executables:
87
87
  - console
88
88
  - setup
89
+ - veil-dump-secrets
89
90
  - veil-env-helper
90
91
  - veil-ingest-secret
91
92
  extensions: []
@@ -94,9 +95,13 @@ files:
94
95
  - LICENSE
95
96
  - bin/console
96
97
  - bin/setup
98
+ - bin/veil-dump-secrets
97
99
  - bin/veil-env-helper
98
100
  - bin/veil-ingest-secret
99
101
  - lib/veil.rb
102
+ - lib/veil/cipher.rb
103
+ - lib/veil/cipher/v1.rb
104
+ - lib/veil/cipher/v2.rb
100
105
  - lib/veil/credential.rb
101
106
  - lib/veil/credential_collection.rb
102
107
  - lib/veil/credential_collection/base.rb
@@ -108,6 +113,9 @@ files:
108
113
  - lib/veil/hasher/pbkdf2.rb
109
114
  - lib/veil/utils.rb
110
115
  - lib/veil/version.rb
116
+ - spec/cipher/v1_spec.rb
117
+ - spec/cipher/v2_spec.rb
118
+ - spec/cipher_spec.rb
111
119
  - spec/credential_collection/base_spec.rb
112
120
  - spec/credential_collection/chef_secrets_file_spec.rb
113
121
  - spec/credential_collection_spec.rb
@@ -139,11 +147,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
147
  version: '0'
140
148
  requirements: []
141
149
  rubyforge_project:
142
- rubygems_version: 2.6.10
150
+ rubygems_version: 2.4.5.2
143
151
  signing_key:
144
152
  specification_version: 4
145
153
  summary: Veil is a Ruby Gem for generating secure secrets from a shared secret
146
154
  test_files:
155
+ - spec/cipher/v1_spec.rb
156
+ - spec/cipher/v2_spec.rb
157
+ - spec/cipher_spec.rb
147
158
  - spec/credential_collection/base_spec.rb
148
159
  - spec/credential_collection/chef_secrets_file_spec.rb
149
160
  - spec/credential_collection_spec.rb