veil 0.2.0 → 0.3.0

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