slosilo 0.0.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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +10 -0
  3. data/.gitignore +21 -0
  4. data/.gitleaks.toml +221 -0
  5. data/.kateproject +4 -0
  6. data/CHANGELOG.md +50 -0
  7. data/CONTRIBUTING.md +16 -0
  8. data/Gemfile +4 -0
  9. data/Jenkinsfile +132 -0
  10. data/LICENSE +22 -0
  11. data/README.md +152 -0
  12. data/Rakefile +17 -0
  13. data/SECURITY.md +42 -0
  14. data/dev/Dockerfile.dev +7 -0
  15. data/dev/docker-compose.yml +8 -0
  16. data/lib/slosilo/adapters/abstract_adapter.rb +23 -0
  17. data/lib/slosilo/adapters/file_adapter.rb +42 -0
  18. data/lib/slosilo/adapters/memory_adapter.rb +31 -0
  19. data/lib/slosilo/adapters/mock_adapter.rb +21 -0
  20. data/lib/slosilo/adapters/sequel_adapter/migration.rb +52 -0
  21. data/lib/slosilo/adapters/sequel_adapter.rb +96 -0
  22. data/lib/slosilo/attr_encrypted.rb +85 -0
  23. data/lib/slosilo/errors.rb +15 -0
  24. data/lib/slosilo/jwt.rb +122 -0
  25. data/lib/slosilo/key.rb +218 -0
  26. data/lib/slosilo/keystore.rb +89 -0
  27. data/lib/slosilo/random.rb +11 -0
  28. data/lib/slosilo/symmetric.rb +63 -0
  29. data/lib/slosilo/version.rb +22 -0
  30. data/lib/slosilo.rb +13 -0
  31. data/lib/tasks/slosilo.rake +32 -0
  32. data/publish.sh +5 -0
  33. data/secrets.yml +1 -0
  34. data/slosilo.gemspec +38 -0
  35. data/spec/encrypted_attributes_spec.rb +114 -0
  36. data/spec/file_adapter_spec.rb +81 -0
  37. data/spec/jwt_spec.rb +102 -0
  38. data/spec/key_spec.rb +258 -0
  39. data/spec/keystore_spec.rb +26 -0
  40. data/spec/random_spec.rb +19 -0
  41. data/spec/sequel_adapter_spec.rb +171 -0
  42. data/spec/slosilo_spec.rb +124 -0
  43. data/spec/spec_helper.rb +84 -0
  44. data/spec/symmetric_spec.rb +94 -0
  45. data/test.sh +8 -0
  46. metadata +238 -0
@@ -0,0 +1,218 @@
1
+ require 'openssl'
2
+ require 'json'
3
+ require 'base64'
4
+ require 'time'
5
+
6
+ require 'slosilo/errors'
7
+
8
+ module Slosilo
9
+ class Key
10
+ def initialize raw_key = nil
11
+ @key = if raw_key.is_a? OpenSSL::PKey::RSA
12
+ raw_key
13
+ elsif !raw_key.nil?
14
+ OpenSSL::PKey.read raw_key
15
+ else
16
+ OpenSSL::PKey::RSA.new 2048
17
+ end
18
+ rescue OpenSSL::PKey::PKeyError => e
19
+ # old openssl versions used to report ArgumentError
20
+ # which arguably makes more sense here, so reraise as that
21
+ raise ArgumentError, e, e.backtrace
22
+ end
23
+
24
+ attr_reader :key
25
+
26
+ def cipher
27
+ @cipher ||= Slosilo::Symmetric.new
28
+ end
29
+
30
+ def encrypt plaintext
31
+ key = cipher.random_key
32
+ ctxt = cipher.encrypt plaintext, key: key
33
+ key = @key.public_encrypt key
34
+ [ctxt, key]
35
+ end
36
+
37
+ def encrypt_message plaintext
38
+ c, k = encrypt plaintext
39
+ k + c
40
+ end
41
+
42
+ def decrypt ciphertext, skey
43
+ key = @key.private_decrypt skey
44
+ cipher.decrypt ciphertext, key: key
45
+ end
46
+
47
+ def decrypt_message ciphertext
48
+ k, c = ciphertext.unpack("A256A*")
49
+ decrypt c, k
50
+ end
51
+
52
+ def to_s
53
+ @key.public_key.to_pem
54
+ end
55
+
56
+ def to_der
57
+ @to_der ||= @key.to_der
58
+ end
59
+
60
+ def sign value
61
+ sign_string(stringify value)
62
+ end
63
+
64
+ SIGNATURE_LEN = 256
65
+
66
+ def verify_signature data, signature
67
+ signature, salt = signature.unpack("a#{SIGNATURE_LEN}a*")
68
+ key.public_decrypt(signature) == hash_function.digest(salt + stringify(data))
69
+ rescue
70
+ false
71
+ end
72
+
73
+ # create a new timestamped and signed token carrying data
74
+ def signed_token data
75
+ token = { "data" => data, "timestamp" => Time.new.utc.to_s }
76
+ token["signature"] = Base64::urlsafe_encode64(sign token)
77
+ token["key"] = fingerprint
78
+ token
79
+ end
80
+
81
+ JWT_ALGORITHM = 'conjur.org/slosilo/v2'.freeze
82
+
83
+ # Issue a JWT with the given claims.
84
+ # `iat` (issued at) claim is automatically added.
85
+ # Other interesting claims you can give are:
86
+ # - `sub` - token subject, for example a user name;
87
+ # - `exp` - expiration time (absolute);
88
+ # - `cidr` (Conjur extension) - array of CIDR masks that are accepted to
89
+ # make requests that bear this token
90
+ def issue_jwt claims
91
+ token = Slosilo::JWT.new claims
92
+ token.add_signature \
93
+ alg: JWT_ALGORITHM,
94
+ kid: fingerprint,
95
+ &method(:sign)
96
+ token.freeze
97
+ end
98
+
99
+ DEFAULT_EXPIRATION = 8 * 60
100
+
101
+ def token_valid? token, expiry = DEFAULT_EXPIRATION
102
+ return jwt_valid? token if token.respond_to? :header
103
+ token = token.clone
104
+ expected_key = token.delete "key"
105
+ return false if (expected_key and (expected_key != fingerprint))
106
+ signature = Base64::urlsafe_decode64(token.delete "signature")
107
+ (Time.parse(token["timestamp"]) + expiry > Time.now) && verify_signature(token, signature)
108
+ end
109
+
110
+ # Validate a JWT.
111
+ #
112
+ # Convenience method calling #validate_jwt and returning false if an
113
+ # exception is raised.
114
+ #
115
+ # @param token [JWT] pre-parsed token to verify
116
+ # @return [Boolean]
117
+ def jwt_valid? token
118
+ validate_jwt token
119
+ true
120
+ rescue
121
+ false
122
+ end
123
+
124
+ # Validate a JWT.
125
+ #
126
+ # First checks whether algorithm is 'conjur.org/slosilo/v2' and the key id
127
+ # matches this key's fingerprint. Then verifies if the token is not expired,
128
+ # as indicated by the `exp` claim; in its absence tokens are assumed to
129
+ # expire in `iat` + 8 minutes.
130
+ #
131
+ # If those checks pass, finally the signature is verified.
132
+ #
133
+ # @raises TokenValidationError if any of the checks fail.
134
+ #
135
+ # @note It's the responsibility of the caller to examine other claims
136
+ # included in the token; consideration needs to be given to handling
137
+ # unrecognized claims.
138
+ #
139
+ # @param token [JWT] pre-parsed token to verify
140
+ def validate_jwt token
141
+ def err msg
142
+ raise Error::TokenValidationError, msg, caller
143
+ end
144
+
145
+ header = token.header
146
+ err 'unrecognized algorithm' unless header['alg'] == JWT_ALGORITHM
147
+ err 'mismatched key' if (kid = header['kid']) && kid != fingerprint
148
+ iat = Time.at token.claims['iat'] || err('unknown issuing time')
149
+ exp = Time.at token.claims['exp'] || (iat + DEFAULT_EXPIRATION)
150
+ err 'token expired' if exp <= Time.now
151
+ err 'invalid signature' unless verify_signature token.string_to_sign, token.signature
152
+ true
153
+ end
154
+
155
+ def sign_string value
156
+ salt = shake_salt
157
+ key.private_encrypt(hash_function.digest(salt + value)) + salt
158
+ end
159
+
160
+ def fingerprint
161
+ @fingerprint ||= OpenSSL::Digest::SHA256.hexdigest key.public_key.to_der
162
+ end
163
+
164
+ def == other
165
+ to_der == other.to_der
166
+ end
167
+
168
+ alias_method :eql?, :==
169
+
170
+ def hash
171
+ to_der.hash
172
+ end
173
+
174
+ # return a new key with just the public part of this
175
+ def public
176
+ Key.new(@key.public_key)
177
+ end
178
+
179
+ # checks if the keypair contains a private key
180
+ def private?
181
+ @key.private?
182
+ end
183
+
184
+ private
185
+
186
+ # Note that this is currently somewhat shallow stringification --
187
+ # to implement originating tokens we may need to make it deeper.
188
+ def stringify value
189
+ string = case value
190
+ when Hash
191
+ value.to_a.sort.to_json
192
+ when String
193
+ value
194
+ else
195
+ value.to_json
196
+ end
197
+
198
+ # Make sure that the string is ascii_8bit (i.e. raw bytes), and represents
199
+ # the utf-8 encoding of the string. This accomplishes two things: it normalizes
200
+ # the representation of the string at the byte level (so we don't have an error if
201
+ # one username is submitted as ISO-whatever, and the next as UTF-16), and it prevents
202
+ # an incompatible encoding error when we concatenate it with the salt.
203
+ if string.encoding != Encoding::ASCII_8BIT
204
+ string.encode(Encoding::UTF_8).force_encoding(Encoding::ASCII_8BIT)
205
+ else
206
+ string
207
+ end
208
+ end
209
+
210
+ def shake_salt
211
+ Slosilo::Random::salt
212
+ end
213
+
214
+ def hash_function
215
+ @hash_function ||= OpenSSL::Digest::SHA256
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,89 @@
1
+ require 'slosilo/key'
2
+
3
+ module Slosilo
4
+ class Keystore
5
+ def adapter
6
+ Slosilo::adapter or raise "No Slosilo adapter is configured or available"
7
+ end
8
+
9
+ def put id, key
10
+ id = id.to_s
11
+ fail ArgumentError, "id can't be empty" if id.empty?
12
+ adapter.put_key id, key
13
+ end
14
+
15
+ def get opts
16
+ id, fingerprint = opts.is_a?(Hash) ? [nil, opts[:fingerprint]] : [opts, nil]
17
+ if id
18
+ key = adapter.get_key(id.to_s)
19
+ elsif fingerprint
20
+ key, _ = get_by_fingerprint(fingerprint)
21
+ end
22
+ key
23
+ end
24
+
25
+ def get_by_fingerprint fingerprint
26
+ adapter.get_by_fingerprint fingerprint
27
+ end
28
+
29
+ def each &_
30
+ adapter.each { |k, v| yield k, v }
31
+ end
32
+
33
+ def any? &block
34
+ each do |_, k|
35
+ return true if yield k
36
+ end
37
+ return false
38
+ end
39
+ end
40
+
41
+ class << self
42
+ def []= id, value
43
+ keystore.put id, value
44
+ end
45
+
46
+ def [] id
47
+ keystore.get id
48
+ end
49
+
50
+ def each(&block)
51
+ keystore.each(&block)
52
+ end
53
+
54
+ def sign object
55
+ self[:own].sign object
56
+ end
57
+
58
+ def token_valid? token
59
+ keystore.any? { |k| k.token_valid? token }
60
+ end
61
+
62
+ # Looks up the signer by public key fingerprint and checks the validity
63
+ # of the signature. If the token is JWT, exp and/or iat claims are also
64
+ # verified; the caller is responsible for validating any other claims.
65
+ def token_signer token
66
+ begin
67
+ # see if maybe it's a JWT
68
+ token = JWT token
69
+ fingerprint = token.header['kid']
70
+ rescue ArgumentError
71
+ fingerprint = token['key']
72
+ end
73
+
74
+ key, id = keystore.get_by_fingerprint fingerprint
75
+ if key && key.token_valid?(token)
76
+ return id
77
+ else
78
+ return nil
79
+ end
80
+ end
81
+
82
+ attr_accessor :adapter
83
+
84
+ private
85
+ def keystore
86
+ @keystore ||= Keystore.new
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,11 @@
1
+ require 'openssl'
2
+
3
+ module Slosilo
4
+ module Random
5
+ class << self
6
+ def salt
7
+ OpenSSL::Random::random_bytes 32
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ module Slosilo
2
+ class Symmetric
3
+ VERSION_MAGIC = 'G'
4
+ TAG_LENGTH = 16
5
+
6
+ def initialize
7
+ @cipher = OpenSSL::Cipher.new 'aes-256-gcm' # NB: has to be lower case for whatever reason.
8
+ @cipher_mutex = Mutex.new
9
+ end
10
+
11
+ # This lets us do a final sanity check in migrations from older encryption versions
12
+ def cipher_name
13
+ @cipher.name
14
+ end
15
+
16
+ def encrypt plaintext, opts = {}
17
+ # All of these operations in OpenSSL must occur atomically, so we
18
+ # synchronize their access to make this step thread-safe.
19
+ @cipher_mutex.synchronize do
20
+ @cipher.reset
21
+ @cipher.encrypt
22
+ @cipher.key = (opts[:key] or raise("missing :key option"))
23
+ @cipher.iv = iv = random_iv
24
+ @cipher.auth_data = opts[:aad] || "" # Nothing good happens if you set this to nil, or don't set it at all
25
+ ctext = @cipher.update(plaintext) + @cipher.final
26
+ tag = @cipher.auth_tag(TAG_LENGTH)
27
+ "#{VERSION_MAGIC}#{tag}#{iv}#{ctext}"
28
+ end
29
+ end
30
+
31
+ def decrypt ciphertext, opts = {}
32
+ version, tag, iv, ctext = unpack ciphertext
33
+
34
+ raise "Invalid version magic: expected #{VERSION_MAGIC} but was #{version}" unless version == VERSION_MAGIC
35
+
36
+ # All of these operations in OpenSSL must occur atomically, so we
37
+ # synchronize their access to make this step thread-safe.
38
+ @cipher_mutex.synchronize do
39
+ @cipher.reset
40
+ @cipher.decrypt
41
+ @cipher.key = opts[:key]
42
+ @cipher.iv = iv
43
+ @cipher.auth_tag = tag
44
+ @cipher.auth_data = opts[:aad] || ""
45
+ @cipher.update(ctext) + @cipher.final
46
+ end
47
+ end
48
+
49
+ def random_iv
50
+ @cipher.random_iv
51
+ end
52
+
53
+ def random_key
54
+ @cipher.random_key
55
+ end
56
+
57
+ private
58
+ # return tag, iv, ctext
59
+ def unpack msg
60
+ msg.unpack "aa#{TAG_LENGTH}a#{@cipher.iv_len}a*"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,22 @@
1
+ # Copyright 2013-2021 Conjur Inc.
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ # this software and associated documentation files (the "Software"), to deal in
5
+ # the Software without restriction, including without limitation the rights to
6
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ # the Software, and to permit persons to whom the Software is furnished to do so,
8
+ # subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+
20
+ module Slosilo
21
+ VERSION = File.read(File.expand_path('../../VERSION', __dir__))
22
+ end
data/lib/slosilo.rb ADDED
@@ -0,0 +1,13 @@
1
+ require "slosilo/jwt"
2
+ require "slosilo/version"
3
+ require "slosilo/keystore"
4
+ require "slosilo/symmetric"
5
+ require "slosilo/attr_encrypted"
6
+ require "slosilo/random"
7
+ require "slosilo/errors"
8
+
9
+ if defined? Sequel
10
+ require 'slosilo/adapters/sequel_adapter'
11
+ Slosilo::adapter = Slosilo::Adapters::SequelAdapter.new
12
+ end
13
+ Dir[File.join(File.dirname(__FILE__), 'tasks/*.rake')].each { |ext| load ext } if defined?(Rake)
@@ -0,0 +1,32 @@
1
+ namespace :slosilo do
2
+ desc "Dump a public key"
3
+ task :dump, [:name] => :environment do |t, args|
4
+ args.with_defaults(:name => :own)
5
+ puts Slosilo[args[:name]]
6
+ end
7
+
8
+ desc "Enroll a key"
9
+ task :enroll, [:name] => :environment do |t, args|
10
+ key = Slosilo::Key.new STDIN.read
11
+ Slosilo[args[:name]] = key
12
+ puts key
13
+ end
14
+
15
+ desc "Generate a key pair"
16
+ task :generate, [:name] => :environment do |t, args|
17
+ args.with_defaults(:name => :own)
18
+ key = Slosilo::Key.new
19
+ Slosilo[args[:name]] = key
20
+ puts key
21
+ end
22
+
23
+ desc "Migrate to a new database schema"
24
+ task :migrate => :environment do |t|
25
+ Slosilo.adapter.migrate!
26
+ end
27
+
28
+ desc "Recalculate fingerprints in keystore"
29
+ task :recalculate_fingerprints => :environment do |t|
30
+ Slosilo.adapter.recalculate_fingerprints
31
+ end
32
+ end
data/publish.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ summon --yaml "RUBYGEMS_API_KEY: !var rubygems/api-key" \
5
+ publish-rubygem slosilo
data/secrets.yml ADDED
@@ -0,0 +1 @@
1
+ RUBYGEMS_API_KEY: !var rubygems/api-key
data/slosilo.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # -*- encoding: utf-8 -*-
2
+ begin
3
+ require File.expand_path('lib/slosilo/version.rb', __FILE__)
4
+ rescue LoadError
5
+ # so that bundle can be run without the app code
6
+ module Slosilo
7
+ VERSION = '0.0.0'
8
+ end
9
+ end
10
+
11
+ Gem::Specification.new do |gem|
12
+ gem.authors = ["Rafa\305\202 Rzepecki"]
13
+ gem.email = ["divided.mind@gmail.com"]
14
+ gem.description = %q{This gem provides an easy way of storing and retrieving encryption keys in the database.}
15
+ gem.summary = %q{Store SSL keys in a database}
16
+ gem.homepage = "https://github.cyberng.com/Conjur-Enterprise/slosilo/"
17
+ gem.license = "MIT"
18
+
19
+ gem.files = `git ls-files`.split($\)
20
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
21
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
+ gem.name = "slosilo"
23
+ gem.require_paths = ["lib"]
24
+ gem.version = Slosilo::VERSION
25
+ gem.required_ruby_version = '>= 3.0.0'
26
+
27
+ gem.add_development_dependency 'rake'
28
+ gem.add_development_dependency 'rspec', '~> 3.0'
29
+ gem.add_development_dependency 'ci_reporter_rspec'
30
+ gem.add_development_dependency 'simplecov'
31
+ gem.add_development_dependency 'simplecov-cobertura'
32
+ gem.add_development_dependency 'io-grab', '~> 0.0.1'
33
+ gem.add_development_dependency 'sequel' # for sequel tests
34
+ gem.add_development_dependency 'sqlite3' # for sequel tests
35
+ gem.add_development_dependency 'bigdecimal' # for activesupport
36
+ gem.add_development_dependency 'activesupport' # for convenience in specs
37
+ end
38
+
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+ require 'slosilo/attr_encrypted'
3
+
4
+ describe Slosilo::EncryptedAttributes do
5
+ before(:all) do
6
+ Slosilo::encryption_key = OpenSSL::Cipher.new("aes-256-gcm").random_key
7
+ end
8
+
9
+ let(:aad) { proc{ |_| "hithere" } }
10
+
11
+ let(:base){
12
+ Class.new do
13
+ attr_accessor :normal_ivar,:with_aad
14
+ def stupid_ivar
15
+ side_effect!
16
+ @_explicit
17
+ end
18
+ def stupid_ivar= e
19
+ side_effect!
20
+ @_explicit = e
21
+ end
22
+ def side_effect!
23
+
24
+ end
25
+ end
26
+ }
27
+
28
+ let(:sub){
29
+ Class.new(base) do
30
+ attr_encrypted :normal_ivar, :stupid_ivar
31
+ end
32
+ }
33
+
34
+ subject{ sub.new }
35
+
36
+ context "when setting a normal ivar" do
37
+ let(:value){ "some value" }
38
+ it "stores an encrypted value in the ivar" do
39
+ subject.normal_ivar = value
40
+ expect(subject.instance_variable_get(:"@normal_ivar")).to_not eq(value)
41
+ end
42
+
43
+ it "recovers the value set" do
44
+ subject.normal_ivar = value
45
+ expect(subject.normal_ivar).to eq(value)
46
+ end
47
+ end
48
+
49
+ context "when setting an attribute with an implementation" do
50
+ it "calls the base class method" do
51
+ expect(subject).to receive_messages(:side_effect! => nil)
52
+ subject.stupid_ivar = "hi"
53
+ expect(subject.stupid_ivar).to eq("hi")
54
+ end
55
+ end
56
+
57
+ context "when given an :aad option" do
58
+
59
+ let(:cipher){ Slosilo::EncryptedAttributes.cipher }
60
+ let(:key){ Slosilo::EncryptedAttributes.key}
61
+ context "that is a string" do
62
+ let(:aad){ "hello there" }
63
+ before{ sub.attr_encrypted :with_aad, aad: aad }
64
+ it "encrypts the value with the given string for auth data" do
65
+ expect(cipher).to receive(:encrypt).with("hello", key: key, aad: aad)
66
+ subject.with_aad = "hello"
67
+ end
68
+
69
+ it "decrypts the encrypted value" do
70
+ subject.with_aad = "foo"
71
+ expect(subject.with_aad).to eq("foo")
72
+ end
73
+ end
74
+
75
+ context "that is nil" do
76
+ let(:aad){ nil }
77
+ before{ sub.attr_encrypted :with_aad, aad: aad }
78
+ it "encrypts the value with an empty string for auth data" do
79
+ expect(cipher).to receive(:encrypt).with("hello",key: key, aad: "").and_call_original
80
+ subject.with_aad = "hello"
81
+ end
82
+
83
+ it "decrypts the encrypted value" do
84
+ subject.with_aad = "hello"
85
+ expect(subject.with_aad).to eq("hello")
86
+ end
87
+ end
88
+
89
+ context "that is a proc" do
90
+ let(:aad){
91
+ proc{ |o| "x" }
92
+ }
93
+
94
+ before{ sub.attr_encrypted :with_aad, aad: aad }
95
+
96
+ it "calls the proc with the object being encrypted" do
97
+ expect(aad).to receive(:[]).with(subject).and_call_original
98
+ subject.with_aad = "hi"
99
+ end
100
+
101
+ it "encrypts the value with the string returned for auth data" do
102
+ expect(cipher).to receive(:encrypt).with("hello", key: key, aad: aad[subject]).and_call_original
103
+ subject.with_aad = "hello"
104
+ end
105
+ it "decrypts the encrypted value" do
106
+ subject.with_aad = "hello"
107
+ expect(subject.with_aad).to eq("hello")
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+
114
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+ require 'tmpdir'
3
+
4
+ require 'slosilo/adapters/file_adapter'
5
+
6
+ describe Slosilo::Adapters::FileAdapter do
7
+ include_context "with example key"
8
+
9
+ let(:dir) { Dir.mktmpdir }
10
+ let(:adapter) { Slosilo::Adapters::FileAdapter.new dir }
11
+ subject { adapter }
12
+
13
+ describe "#get_key" do
14
+ context "when given key does not exist" do
15
+ it "returns nil" do
16
+ expect(subject.get_key(:whatever)).not_to be
17
+ end
18
+ end
19
+ end
20
+
21
+ describe "#put_key" do
22
+ context "unacceptable id" do
23
+ let(:id) { "foo.bar" }
24
+ it "isn't accepted" do
25
+ expect { subject.put_key id, key }.to raise_error /id should not contain a period/
26
+ end
27
+ end
28
+ context "acceptable id" do
29
+ let(:id) { "id" }
30
+ let(:key_encrypted) { "encrypted key" }
31
+ let(:fname) { "#{dir}/#{id}.key" }
32
+ it "creates the key" do
33
+ expect(Slosilo::EncryptedAttributes).to receive(:encrypt).with(key.to_der).and_return key_encrypted
34
+ expect(File).to receive(:write).with(fname, key_encrypted)
35
+ expect(File).to receive(:chmod).with(0400, fname)
36
+ subject.put_key id, key
37
+ expect(subject.instance_variable_get("@keys")[id]).to eq(key)
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "#each" do
43
+ before { adapter.instance_variable_set("@keys", one: :onek, two: :twok) }
44
+
45
+ it "iterates over each key" do
46
+ results = []
47
+ adapter.each { |id,k| results << { id => k } }
48
+ expect(results).to eq([ { one: :onek}, {two: :twok } ])
49
+ end
50
+ end
51
+
52
+ context 'with real key store' do
53
+ let(:id) { 'some id' }
54
+
55
+ before do
56
+ Slosilo::encryption_key = Slosilo::Symmetric.new.random_key
57
+ pre_adapter = Slosilo::Adapters::FileAdapter.new dir
58
+ pre_adapter.put_key(id, key)
59
+ end
60
+
61
+ describe '#get_key' do
62
+ it "loads and decrypts the key" do
63
+ expect(adapter.get_key(id)).to eq(key)
64
+ end
65
+ end
66
+
67
+ describe '#get_by_fingerprint' do
68
+ it "can look up a key by a fingerprint" do
69
+ expect(adapter.get_by_fingerprint(key_fingerprint)).to eq([key, id])
70
+ end
71
+ end
72
+
73
+ describe '#each' do
74
+ it "enumerates the keys" do
75
+ results = []
76
+ adapter.each { |id,k| results << { id => k } }
77
+ expect(results).to eq([ { id => key } ])
78
+ end
79
+ end
80
+ end
81
+ end