slosilo 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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