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.
- checksums.yaml +7 -0
- data/.github/CODEOWNERS +10 -0
- data/.gitignore +21 -0
- data/.gitleaks.toml +221 -0
- data/.kateproject +4 -0
- data/CHANGELOG.md +50 -0
- data/CONTRIBUTING.md +16 -0
- data/Gemfile +4 -0
- data/Jenkinsfile +132 -0
- data/LICENSE +22 -0
- data/README.md +152 -0
- data/Rakefile +17 -0
- data/SECURITY.md +42 -0
- data/dev/Dockerfile.dev +7 -0
- data/dev/docker-compose.yml +8 -0
- data/lib/slosilo/adapters/abstract_adapter.rb +23 -0
- data/lib/slosilo/adapters/file_adapter.rb +42 -0
- data/lib/slosilo/adapters/memory_adapter.rb +31 -0
- data/lib/slosilo/adapters/mock_adapter.rb +21 -0
- data/lib/slosilo/adapters/sequel_adapter/migration.rb +52 -0
- data/lib/slosilo/adapters/sequel_adapter.rb +96 -0
- data/lib/slosilo/attr_encrypted.rb +85 -0
- data/lib/slosilo/errors.rb +15 -0
- data/lib/slosilo/jwt.rb +122 -0
- data/lib/slosilo/key.rb +218 -0
- data/lib/slosilo/keystore.rb +89 -0
- data/lib/slosilo/random.rb +11 -0
- data/lib/slosilo/symmetric.rb +63 -0
- data/lib/slosilo/version.rb +22 -0
- data/lib/slosilo.rb +13 -0
- data/lib/tasks/slosilo.rake +32 -0
- data/publish.sh +5 -0
- data/secrets.yml +1 -0
- data/slosilo.gemspec +38 -0
- data/spec/encrypted_attributes_spec.rb +114 -0
- data/spec/file_adapter_spec.rb +81 -0
- data/spec/jwt_spec.rb +102 -0
- data/spec/key_spec.rb +258 -0
- data/spec/keystore_spec.rb +26 -0
- data/spec/random_spec.rb +19 -0
- data/spec/sequel_adapter_spec.rb +171 -0
- data/spec/slosilo_spec.rb +124 -0
- data/spec/spec_helper.rb +84 -0
- data/spec/symmetric_spec.rb +94 -0
- data/test.sh +8 -0
- metadata +238 -0
data/lib/slosilo/key.rb
ADDED
@@ -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,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
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
|