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.
@@ -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
@@ -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
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
+ require "veil"
@@ -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