veil 0.2.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 +7 -0
- data/LICENSE +201 -0
- data/bin/console +6 -0
- data/bin/setup +8 -0
- data/bin/veil-env-helper +47 -0
- data/bin/veil-ingest-secret +41 -0
- data/lib/veil.rb +6 -0
- data/lib/veil/credential.rb +75 -0
- data/lib/veil/credential_collection.rb +18 -0
- data/lib/veil/credential_collection/base.rb +240 -0
- data/lib/veil/credential_collection/chef_secrets_file.rb +105 -0
- data/lib/veil/exceptions.rb +15 -0
- data/lib/veil/hasher.rb +29 -0
- data/lib/veil/hasher/base.rb +44 -0
- data/lib/veil/hasher/bcrypt.rb +59 -0
- data/lib/veil/hasher/pbkdf2.rb +54 -0
- data/lib/veil/utils.rb +25 -0
- data/lib/veil/version.rb +3 -0
- data/spec/credential_collection/base_spec.rb +433 -0
- data/spec/credential_collection/chef_secrets_file_spec.rb +142 -0
- data/spec/credential_collection_spec.rb +30 -0
- data/spec/credential_spec.rb +94 -0
- data/spec/hasher/base_spec.rb +19 -0
- data/spec/hasher/bcrypt_spec.rb +48 -0
- data/spec/hasher/pbkdf2_spec.rb +60 -0
- data/spec/hasher_spec.rb +34 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/utils_spec.rb +25 -0
- data/spec/veil_spec.rb +7 -0
- metadata +157 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "tempfile"
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
describe Veil::CredentialCollection::ChefSecretsFile do
|
6
|
+
let(:hasher) { Veil::Hasher.create }
|
7
|
+
let!(:file) { Tempfile.new("private_chef_secrets.json") }
|
8
|
+
let(:user) { "opscode_user" }
|
9
|
+
let(:group) { "opscode_group" }
|
10
|
+
let(:content) do
|
11
|
+
{
|
12
|
+
"veil" => {
|
13
|
+
"type" => "Veil::CredentialCollection::ChefSecretsFile",
|
14
|
+
"hasher" => {},
|
15
|
+
"credentials" => {}
|
16
|
+
}
|
17
|
+
}
|
18
|
+
end
|
19
|
+
let(:legacy_content) do
|
20
|
+
{
|
21
|
+
"redis_lb" => {
|
22
|
+
"password" => "f1ad3e8b1e47bc81720742a2572b9ff"
|
23
|
+
},
|
24
|
+
"rabbitmq" => {
|
25
|
+
"password" => "f5031e56ae018a7b71ce153086fc88f",
|
26
|
+
"actions_password" => "92609a68d03b50afcc597d7"
|
27
|
+
}
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#self.from_file" do
|
32
|
+
context "when the file exists" do
|
33
|
+
context "when it's a valid credential store" do
|
34
|
+
it "returns an instance" do
|
35
|
+
file.write(JSON.pretty_generate(content))
|
36
|
+
file.rewind
|
37
|
+
expect(described_class.from_file(file.path)).to be_instance_of(described_class)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "when it's not a valid credential store" do
|
42
|
+
it "raises an error" do
|
43
|
+
file.write("not a json chef secrets file")
|
44
|
+
file.rewind
|
45
|
+
expect { described_class.from_file(file.path) }.to raise_error(Veil::InvalidCredentialCollectionFile)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "when it's a legacy secrets file" do
|
50
|
+
it "imports the legacy file" do
|
51
|
+
file.write(JSON.pretty_generate(legacy_content))
|
52
|
+
file.rewind
|
53
|
+
instance = described_class.from_file(file.path)
|
54
|
+
expect(instance["redis_lb"]["password"].value).to eq("f1ad3e8b1e47bc81720742a2572b9ff")
|
55
|
+
expect(instance["redis_lb"]["password"].version).to eq(0)
|
56
|
+
expect(instance["redis_lb"]["password"].length).to eq(31)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when the file doesn't exist" do
|
62
|
+
it "raises an error" do
|
63
|
+
expect { described_class.from_file("not_a_file") }.to raise_error(Veil::InvalidCredentialCollectionFile)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#save" do
|
69
|
+
it "saves the content to a machine loadable file" do
|
70
|
+
file.rewind
|
71
|
+
creds = described_class.new(path: file.path)
|
72
|
+
creds.add("redis_lb", "password")
|
73
|
+
creds.add("postgresql", "sql_ro_password")
|
74
|
+
creds.save
|
75
|
+
|
76
|
+
file.rewind
|
77
|
+
new_creds = described_class.from_file(file.path)
|
78
|
+
|
79
|
+
expect(new_creds["redis_lb"]["password"].value).to eq(creds["redis_lb"]["password"].value)
|
80
|
+
expect(new_creds["postgresql"]["sql_ro_password"].value).to eq(creds["postgresql"]["sql_ro_password"].value)
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when using ownership management" do
|
84
|
+
let(:tmpfile) do
|
85
|
+
s = StringIO.new
|
86
|
+
allow(s).to receive(:path).and_return("/tmp/unguessable")
|
87
|
+
s
|
88
|
+
end
|
89
|
+
|
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)
|
95
|
+
|
96
|
+
creds = described_class.new(path: file.path,
|
97
|
+
user: user)
|
98
|
+
creds.add("redis_lb", "password")
|
99
|
+
creds.save
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
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
|
114
|
+
end
|
115
|
+
|
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
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
it "saves the version number" do
|
132
|
+
allow(FileUtils).to receive(:chown)
|
133
|
+
file.rewind
|
134
|
+
creds = described_class.new(path: file.path, version: 12)
|
135
|
+
creds.save
|
136
|
+
|
137
|
+
file.rewind
|
138
|
+
new_creds = described_class.from_file(file.path)
|
139
|
+
expect(new_creds.version).to eq(12)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Veil::CredentialCollection do
|
4
|
+
describe 'from_config' do
|
5
|
+
context 'passing provider "chef-secrets-file"' do
|
6
|
+
let(:opts) { { provider: 'chef-secrets-file', something_else: 'config' } }
|
7
|
+
|
8
|
+
it 'instantiates ChefSecretsFile with all options' do
|
9
|
+
expect(Veil::CredentialCollection::ChefSecretsFile).to receive(:new).with(opts)
|
10
|
+
described_class.from_config(opts)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'passing anything else as provider' do
|
15
|
+
let(:opts) { { provider: 'vault' } }
|
16
|
+
|
17
|
+
it 'raises an exception' do
|
18
|
+
expect { described_class.from_config(opts) }.to raise_error(Veil::UnknownProvider)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'passing an options hash that has no provider' do
|
23
|
+
let(:opts) { { something: 'else' } }
|
24
|
+
|
25
|
+
it 'raises an exception' do
|
26
|
+
expect { described_class.from_config(opts) }.to raise_error(Veil::UnknownProvider)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Veil::Credential do
|
4
|
+
let(:data) { "heart and lungs" }
|
5
|
+
let(:salt) { "NaCL" }
|
6
|
+
let(:secret) { "Black Eagle" }
|
7
|
+
let(:hasher_hash) do
|
8
|
+
{
|
9
|
+
data: data,
|
10
|
+
salt: salt,
|
11
|
+
secret: secret
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:credential_hash) do
|
16
|
+
{
|
17
|
+
name: "eros",
|
18
|
+
version: 23,
|
19
|
+
value: "some crazy secret",
|
20
|
+
length: 17,
|
21
|
+
group: "portals"
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:hasher) { Veil::Hasher.create(hasher_hash) }
|
26
|
+
|
27
|
+
subject { described_class.new(name: "eros", value: "thing") }
|
28
|
+
|
29
|
+
describe "self.create" do
|
30
|
+
it "creates an instance from the hash" do
|
31
|
+
cred = described_class.create(credential_hash)
|
32
|
+
expect(cred.name).to eq(credential_hash[:name])
|
33
|
+
expect(cred.version).to eq(credential_hash[:version])
|
34
|
+
expect(cred.value).to eq(credential_hash[:value])
|
35
|
+
expect(cred.length).to eq(credential_hash[:length])
|
36
|
+
expect(cred.group).to eq(credential_hash[:group])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#new" do
|
41
|
+
it "sets a default version to 0" do
|
42
|
+
expect(subject.version).to eq(0)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#rotate!" do
|
47
|
+
it "increments the version and hashes a new value" do
|
48
|
+
cred = described_class.new(group: "foo", name: "bar", version: 2, value: "thing")
|
49
|
+
cred.rotate!(hasher)
|
50
|
+
expect(cred.version).to eq(3)
|
51
|
+
expect(cred.value).to_not eq("thing")
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when the hasher is invalid" do
|
55
|
+
it "raises an invalid hasher error" do
|
56
|
+
expect { subject.rotate!(1) }.to raise_error(Veil::InvalidHasher)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when the credential is frozen" do
|
61
|
+
it "raises a runtime error" do
|
62
|
+
cred = described_class.new(group: "foo", name: "bar", version: 3, value: "thing", frozen: true)
|
63
|
+
expect { cred.rotate!(1) }.to raise_error(RuntimeError)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#rotate" do
|
69
|
+
it "increments the version and hashes a new value" do
|
70
|
+
cred = described_class.new(group: "foo", name: "bar", version: 2, value: "thing")
|
71
|
+
cred.rotate(hasher)
|
72
|
+
expect(cred.version).to eq(3)
|
73
|
+
expect(cred.value).to_not eq("thing")
|
74
|
+
end
|
75
|
+
|
76
|
+
context "when the hasher is invalid" do
|
77
|
+
it "does not rotate the credential" do
|
78
|
+
cred = described_class.new(group: "foo", name: "bar", version: 3, value: "thing", frozen: true)
|
79
|
+
expect(cred.rotate(1)).to eq(false)
|
80
|
+
expect(cred.version).to eq(3)
|
81
|
+
expect(cred.value).to eq("thing")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context "when the credential is frozen" do
|
86
|
+
it "does not rotate the credential" do
|
87
|
+
cred = described_class.new(group: "foo", name: "bar", version: 3, value: "thing", frozen: true)
|
88
|
+
expect(cred.rotate(hasher)).to eq(false)
|
89
|
+
expect(cred.version).to eq(3)
|
90
|
+
expect(cred.value).to eq("thing")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "digest"
|
3
|
+
|
4
|
+
describe Veil::Hasher::Base do
|
5
|
+
let(:data) { "let him enter" }
|
6
|
+
|
7
|
+
subject { described_class.new }
|
8
|
+
|
9
|
+
describe "#hex_digest" do
|
10
|
+
it "returns a SHA512 hex digest of the data" do
|
11
|
+
expect(subject.send(:hex_digest, data)).to eq(OpenSSL::Digest::SHA512.hexdigest(data))
|
12
|
+
end
|
13
|
+
|
14
|
+
it "does not use Digest, which uses low level APIs prohibited by FIPS" do
|
15
|
+
expect(Digest::SHA512).not_to receive(:hexdigest)
|
16
|
+
subject.send(:hex_digest, data)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Veil::Hasher::BCrypt do
|
4
|
+
let(:group) { "postgresql" }
|
5
|
+
let(:name) { "sql_ro_password" }
|
6
|
+
let(:version) { "5" }
|
7
|
+
let(:cost) { 11 }
|
8
|
+
let(:salt) { "$2a$11$4xS0IHHxU5sOYZ0Z5X53Qe" }
|
9
|
+
let(:secret) { "only friends tell each other" }
|
10
|
+
|
11
|
+
subject { described_class.new(secret: secret, salt: salt) }
|
12
|
+
|
13
|
+
describe "#new" do
|
14
|
+
it "builds an instance" do
|
15
|
+
expect(described_class.new.class).to eq(described_class)
|
16
|
+
end
|
17
|
+
|
18
|
+
context "from a hash" do
|
19
|
+
it "builds an identical instance" do
|
20
|
+
new_instance = described_class.new(subject.to_hash)
|
21
|
+
expect(new_instance.encrypt("slow", "forever", 1)).to eq(subject.encrypt("slow", "forever", 1))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#encrypt" do
|
27
|
+
it "deterministically encrypts data" do
|
28
|
+
encrypted_data = subject.encrypt(group, name, version)
|
29
|
+
|
30
|
+
new_instance = described_class.new(
|
31
|
+
secret: secret,
|
32
|
+
salt: salt,
|
33
|
+
)
|
34
|
+
|
35
|
+
expect(new_instance.encrypt(group, name, version)).to eq(encrypted_data)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#to_hash" do
|
40
|
+
it "returns itself as a hash" do
|
41
|
+
expect(subject.to_hash).to eq({
|
42
|
+
type: "Veil::Hasher::BCrypt",
|
43
|
+
secret: secret,
|
44
|
+
salt: salt,
|
45
|
+
})
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Veil::Hasher::PBKDF2 do
|
4
|
+
let(:group) { "android" }
|
5
|
+
let(:name) { "artoo" }
|
6
|
+
let(:version) { 2 }
|
7
|
+
let(:salt) { "nacl" }
|
8
|
+
let(:secret) { "sauce" }
|
9
|
+
let(:iterations) { 100 }
|
10
|
+
let(:digest) { "SHA256" }
|
11
|
+
|
12
|
+
subject do
|
13
|
+
described_class.new(
|
14
|
+
secret: secret,
|
15
|
+
salt: salt,
|
16
|
+
iterations: iterations,
|
17
|
+
hash_function: digest
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#new" do
|
22
|
+
it "builds an instance" do
|
23
|
+
expect(described_class.new.class).to eq(described_class)
|
24
|
+
end
|
25
|
+
|
26
|
+
context "from a hash" do
|
27
|
+
it "builds an identical instance" do
|
28
|
+
new_instance = described_class.new(subject.to_hash)
|
29
|
+
expect(new_instance.encrypt("slow", "forever", 5)).to eq(subject.encrypt("slow", "forever", 5))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#encrypt" do
|
35
|
+
it "deterministically encrypts data" do
|
36
|
+
encrypted_data = subject.encrypt(group, name, version)
|
37
|
+
|
38
|
+
new_instance = described_class.new(
|
39
|
+
secret: secret,
|
40
|
+
salt: salt,
|
41
|
+
iterations: iterations,
|
42
|
+
hash_function: digest
|
43
|
+
)
|
44
|
+
|
45
|
+
expect(new_instance.encrypt(group, name, version)).to eq(encrypted_data)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#to_hash" do
|
50
|
+
it "returns itself as a hash" do
|
51
|
+
expect(subject.to_hash).to eq({
|
52
|
+
type: "Veil::Hasher::PBKDF2",
|
53
|
+
secret: secret,
|
54
|
+
salt: salt,
|
55
|
+
iterations: iterations,
|
56
|
+
hash_function: "OpenSSL::Digest::SHA256"
|
57
|
+
})
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/spec/hasher_spec.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Veil::Hasher do
|
4
|
+
let(:data) { "let him enter" }
|
5
|
+
let(:cost) { 11 }
|
6
|
+
let(:salt) { "$2a$11$4xS0IHHxU5sOYZ0Z5X53Qe" }
|
7
|
+
let(:secret) { "super duper secret" }
|
8
|
+
let(:hash) do
|
9
|
+
{ type: "Veil::Hasher::BCrypt",
|
10
|
+
secret: secret,
|
11
|
+
salt: salt
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#self.create" do
|
16
|
+
context "with opts" do
|
17
|
+
it "returns an instance of the class" do
|
18
|
+
instance = described_class.create(hash)
|
19
|
+
expect(instance.class.name).to eq("Veil::Hasher::BCrypt")
|
20
|
+
expect(instance.secret).to eq(secret)
|
21
|
+
expect(instance.salt).to eq(salt)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "without opts" do
|
26
|
+
it "returns an instance of the default class" do
|
27
|
+
instance = described_class.create
|
28
|
+
expect(instance.class.name).to eq("Veil::Hasher::PBKDF2")
|
29
|
+
expect(instance.iterations).to eq(10_000)
|
30
|
+
expect(instance.hash_function.class.name).to eq("OpenSSL::Digest::SHA512")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/spec/utils_spec.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Veil::Utils do
|
4
|
+
let(:mixed_hash) do
|
5
|
+
{
|
6
|
+
:foo => "bar",
|
7
|
+
bar: "baz",
|
8
|
+
"fizz" => :buzz
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:symbolized_hash) do
|
13
|
+
{
|
14
|
+
foo: "bar",
|
15
|
+
bar: "baz",
|
16
|
+
fizz: :buzz
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#symbolize_keys" do
|
21
|
+
it "symbolizes a hashes keys" do
|
22
|
+
expect(described_class.symbolize_keys(mixed_hash)).to eq(symbolized_hash)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|