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