slosilo 0.0.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +0 -2
  2. data/LICENSE +2 -2
  3. data/README.md +8 -128
  4. data/lib/slosilo/adapters/abstract_adapter.rb +0 -4
  5. data/lib/slosilo/adapters/mock_adapter.rb +1 -14
  6. data/lib/slosilo/adapters/sequel_adapter/migration.rb +2 -5
  7. data/lib/slosilo/adapters/sequel_adapter.rb +5 -67
  8. data/lib/slosilo/attr_encrypted.rb +7 -33
  9. data/lib/slosilo/http_request.rb +59 -0
  10. data/lib/slosilo/key.rb +6 -129
  11. data/lib/slosilo/keystore.rb +12 -40
  12. data/lib/slosilo/rack/middleware.rb +123 -0
  13. data/lib/slosilo/symmetric.rb +17 -47
  14. data/lib/slosilo/version.rb +2 -21
  15. data/lib/slosilo.rb +2 -2
  16. data/lib/tasks/slosilo.rake +0 -10
  17. data/slosilo.gemspec +6 -19
  18. data/spec/http_request_spec.rb +107 -0
  19. data/spec/http_stack_spec.rb +44 -0
  20. data/spec/key_spec.rb +32 -175
  21. data/spec/keystore_spec.rb +2 -15
  22. data/spec/rack_middleware_spec.rb +109 -0
  23. data/spec/random_spec.rb +2 -12
  24. data/spec/sequel_adapter_spec.rb +22 -133
  25. data/spec/slosilo_spec.rb +12 -78
  26. data/spec/spec_helper.rb +15 -37
  27. data/spec/symmetric_spec.rb +26 -69
  28. metadata +51 -104
  29. checksums.yaml +0 -7
  30. data/.github/CODEOWNERS +0 -10
  31. data/.gitleaks.toml +0 -221
  32. data/.kateproject +0 -4
  33. data/CHANGELOG.md +0 -50
  34. data/CONTRIBUTING.md +0 -16
  35. data/Jenkinsfile +0 -132
  36. data/SECURITY.md +0 -42
  37. data/dev/Dockerfile.dev +0 -7
  38. data/dev/docker-compose.yml +0 -8
  39. data/lib/slosilo/adapters/file_adapter.rb +0 -42
  40. data/lib/slosilo/adapters/memory_adapter.rb +0 -31
  41. data/lib/slosilo/errors.rb +0 -15
  42. data/lib/slosilo/jwt.rb +0 -122
  43. data/publish.sh +0 -5
  44. data/secrets.yml +0 -1
  45. data/spec/encrypted_attributes_spec.rb +0 -114
  46. data/spec/file_adapter_spec.rb +0 -81
  47. data/spec/jwt_spec.rb +0 -102
  48. data/test.sh +0 -8
@@ -7,34 +7,26 @@ module Slosilo
7
7
  end
8
8
 
9
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
10
+ adapter.put_key id.to_s, key.to_der
13
11
  end
14
12
 
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
13
+ def get id
14
+ key = adapter.get_key(id.to_s)
15
+ key && Key.new(key)
27
16
  end
28
17
 
29
- def each &_
30
- adapter.each { |k, v| yield k, v }
18
+ def each(&block)
19
+ adapter.each(&block)
31
20
  end
32
21
 
33
22
  def any? &block
34
- each do |_, k|
35
- return true if yield k
23
+ catch :found do
24
+ adapter.each do |id, k|
25
+ throw :found if block.call(Key.new(k))
26
+ end
27
+ return false
36
28
  end
37
- return false
29
+ true
38
30
  end
39
31
  end
40
32
 
@@ -59,26 +51,6 @@ module Slosilo
59
51
  keystore.any? { |k| k.token_valid? token }
60
52
  end
61
53
 
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
54
  attr_accessor :adapter
83
55
 
84
56
  private
@@ -0,0 +1,123 @@
1
+ module Slosilo
2
+ module Rack
3
+ # Con perform verification of request signature and decryption of request body.
4
+ #
5
+ # Signature verification and body decryption are enabled with constructor switches and are
6
+ # therefore performed (or not) for all requests.
7
+ #
8
+ # When signature verification is performed, the following elements are included in the
9
+ # signature string:
10
+ #
11
+ # 1. Request path and query string
12
+ # 2. base64 encoded request body
13
+ # 3. Request timestamp from HTTP_TIMESTAMP
14
+ # 4. Body encryption key from HTTP_X_SLOSILO_KEY (if present)
15
+ #
16
+ # When body decryption is performed, an encryption key for the message body is encrypted
17
+ # with this service's public key and placed in HTTP_X_SLOSILO_KEY. This middleware
18
+ # decryps the key using our :own private key, and then decrypts the body using the decrypted key.
19
+ class Middleware
20
+ class EncryptionError < SecurityError
21
+ end
22
+ class SignatureError < SecurityError
23
+ end
24
+
25
+ def initialize app, opts = {}
26
+ @app = app
27
+ @encryption_required = opts[:encryption_required] || false
28
+ @signature_required = opts[:signature_required] || false
29
+ end
30
+
31
+ def call env
32
+ @env = env
33
+ @body = env['rack.input'].read rescue ""
34
+
35
+ begin
36
+ verify
37
+ decrypt
38
+ rescue EncryptionError
39
+ return error 403, $!.message
40
+ rescue SignatureError
41
+ return error 401, $!.message
42
+ end
43
+
44
+ @app.call env
45
+ end
46
+
47
+ private
48
+ def verify
49
+ if signature
50
+ raise SignatureError, "Bad signature" unless Slosilo.token_valid?(token)
51
+ else
52
+ raise SignatureError, "Signature required" if signature_required?
53
+ end
54
+ end
55
+
56
+ attr_reader :env
57
+
58
+ def token
59
+ return nil unless signature
60
+ t = { "data" => { "path" => path, "body" => [body].pack('m0') }, "timestamp" => timestamp, "signature" => signature }
61
+ t["data"]["key"] = encoded_key if encoded_key
62
+ t['data']['authorization'] = env['HTTP_AUTHORIZATION'] if env['HTTP_AUTHORIZATION']
63
+ t
64
+ end
65
+
66
+ def path
67
+ env['SCRIPT_NAME'] + env['PATH_INFO'] + query_string
68
+ end
69
+
70
+ def query_string
71
+ if env['QUERY_STRING'].empty?
72
+ ''
73
+ else
74
+ '?' + env['QUERY_STRING']
75
+ end
76
+ end
77
+
78
+ attr_reader :body
79
+
80
+ def timestamp
81
+ env['HTTP_TIMESTAMP']
82
+ end
83
+
84
+ def signature
85
+ env['HTTP_X_SLOSILO_SIGNATURE']
86
+ end
87
+
88
+ def encoded_key
89
+ env['HTTP_X_SLOSILO_KEY']
90
+ end
91
+
92
+ def key
93
+ if encoded_key
94
+ Base64::urlsafe_decode64(encoded_key)
95
+ else
96
+ raise EncryptionError, "Encryption required" if encryption_required?
97
+ end
98
+ end
99
+
100
+ def decrypt
101
+ return unless key
102
+ plaintext = Slosilo[:own].decrypt body, key
103
+ env['rack.input'] = StringIO.new plaintext
104
+ rescue EncryptionError
105
+ raise unless body.empty? || body.nil?
106
+ rescue Exception => e
107
+ raise EncryptionError, "Bad encryption", e.backtrace
108
+ end
109
+
110
+ def error status, message
111
+ [status, { 'Content-Type' => 'text/plain', 'Content-Length' => message.length.to_s }, [message] ]
112
+ end
113
+
114
+ def encryption_required?
115
+ @encryption_required
116
+ end
117
+
118
+ def signature_required?
119
+ @signature_required
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,63 +1,33 @@
1
1
  module Slosilo
2
2
  class Symmetric
3
- VERSION_MAGIC = 'G'
4
- TAG_LENGTH = 16
5
-
6
3
  def initialize
7
- @cipher = OpenSSL::Cipher.new 'aes-256-gcm' # NB: has to be lower case for whatever reason.
8
- @cipher_mutex = Mutex.new
4
+ @cipher = OpenSSL::Cipher.new 'AES-256-CBC'
9
5
  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
-
6
+
16
7
  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
8
+ @cipher.reset
9
+ @cipher.encrypt
10
+ @cipher.key = opts[:key]
11
+ @cipher.iv = iv = random_iv
12
+ ctxt = @cipher.update(plaintext)
13
+ iv + ctxt + @cipher.final
29
14
  end
30
-
15
+
31
16
  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
17
+ @cipher.reset
18
+ @cipher.decrypt
19
+ @cipher.key = opts[:key]
20
+ @cipher.iv, ctxt = ciphertext.unpack("a#{@cipher.iv_len}a*")
21
+ ptxt = @cipher.update(ctxt)
22
+ ptxt + @cipher.final
47
23
  end
48
-
24
+
49
25
  def random_iv
50
26
  @cipher.random_iv
51
27
  end
52
-
28
+
53
29
  def random_key
54
30
  @cipher.random_key
55
31
  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
32
  end
63
33
  end
@@ -1,22 +1,3 @@
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
1
  module Slosilo
21
- VERSION = File.read(File.expand_path('../../VERSION', __dir__))
22
- end
2
+ VERSION = "0.1.2"
3
+ end
data/lib/slosilo.rb CHANGED
@@ -1,10 +1,10 @@
1
- require "slosilo/jwt"
2
1
  require "slosilo/version"
3
2
  require "slosilo/keystore"
4
3
  require "slosilo/symmetric"
5
4
  require "slosilo/attr_encrypted"
6
5
  require "slosilo/random"
7
- require "slosilo/errors"
6
+ require "slosilo/rack/middleware"
7
+ require "slosilo/http_request"
8
8
 
9
9
  if defined? Sequel
10
10
  require 'slosilo/adapters/sequel_adapter'
@@ -19,14 +19,4 @@ namespace :slosilo do
19
19
  Slosilo[args[:name]] = key
20
20
  puts key
21
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
22
  end
data/slosilo.gemspec CHANGED
@@ -1,20 +1,12 @@
1
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
2
+ require File.expand_path('../lib/slosilo/version', __FILE__)
10
3
 
11
4
  Gem::Specification.new do |gem|
12
5
  gem.authors = ["Rafa\305\202 Rzepecki"]
13
6
  gem.email = ["divided.mind@gmail.com"]
14
7
  gem.description = %q{This gem provides an easy way of storing and retrieving encryption keys in the database.}
15
8
  gem.summary = %q{Store SSL keys in a database}
16
- gem.homepage = "https://github.cyberng.com/Conjur-Enterprise/slosilo/"
17
- gem.license = "MIT"
9
+ gem.homepage = ""
18
10
 
19
11
  gem.files = `git ls-files`.split($\)
20
12
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -22,17 +14,12 @@ Gem::Specification.new do |gem|
22
14
  gem.name = "slosilo"
23
15
  gem.require_paths = ["lib"]
24
16
  gem.version = Slosilo::VERSION
25
- gem.required_ruby_version = '>= 3.0.0'
26
-
17
+ gem.required_ruby_version = '~> 1.9.3'
18
+
27
19
  gem.add_development_dependency 'rake'
28
- gem.add_development_dependency 'rspec', '~> 3.0'
29
- gem.add_development_dependency 'ci_reporter_rspec'
20
+ gem.add_development_dependency 'rspec'
21
+ gem.add_development_dependency 'ci_reporter'
30
22
  gem.add_development_dependency 'simplecov'
31
- gem.add_development_dependency 'simplecov-cobertura'
32
- gem.add_development_dependency 'io-grab', '~> 0.0.1'
33
23
  gem.add_development_dependency 'sequel' # for sequel tests
34
24
  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
25
  end
38
-
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ describe Slosilo::HTTPRequest do
4
+ let(:keyname) { :bacon }
5
+ let(:encrypt) { subject.encrypt! }
6
+ subject { Hash.new }
7
+ before do
8
+ subject.extend Slosilo::HTTPRequest
9
+ subject.keyname = keyname
10
+ end
11
+
12
+ describe "#sign!" do
13
+ let(:own_key) { double "own key" }
14
+ before { Slosilo.stub(:[]).with(:own).and_return own_key }
15
+
16
+ let(:signed_data) { "this is the truest truth" }
17
+ before { subject.stub signed_data: signed_data }
18
+ let(:timestamp) { "long time ago" }
19
+ let(:signature) { "seal of approval" }
20
+ let(:token) { { "data" => signed_data, "timestamp" => timestamp, "signature" => signature } }
21
+
22
+ it "makes a token out of the data to sign and inserts headers" do
23
+ own_key.stub(:signed_token).with(signed_data).and_return token
24
+ subject.should_receive(:[]=).with 'Timestamp', timestamp
25
+ subject.should_receive(:[]=).with 'X-Slosilo-Signature', signature
26
+ subject.sign!
27
+ end
28
+ end
29
+
30
+ describe "#signed_data" do
31
+ before { subject.stub path: :path, body: 'body' }
32
+ context "when X-Slosilo-Key not present" do
33
+ its(:signed_data) { should == { "path" => :path, "body" => "Ym9keQ==" } }
34
+ end
35
+
36
+ context "when X-Slosilo-Key is present" do
37
+ before { subject.merge! 'X-Slosilo-Key' => :key }
38
+ its(:signed_data) { should == { "path" => :path, "body" => "Ym9keQ==", "key" => :key } }
39
+ end
40
+ end
41
+
42
+ describe "#encrypt!" do
43
+ context "when key not set" do
44
+ before { subject.keyname = nil }
45
+ it "does nothing" do
46
+ subject.should_not_receive(:body=)
47
+ encrypt
48
+ end
49
+ end
50
+
51
+ context "when requested key does not exist" do
52
+ before { Slosilo.stub(:[]).and_return nil }
53
+ it "raises error" do
54
+ expect{ encrypt }.to raise_error
55
+ end
56
+ end
57
+
58
+ context "when the key exists" do
59
+ let(:key) { double "key" }
60
+ context "when the body is not empty" do
61
+ let(:plaintext) { "Keep your solutions close, and your problems closer." }
62
+ let(:ciphertext) { "And, when you want something, all the universe conspires in helping you to achieve it." }
63
+ let(:skey) { "make me sound like a fool instead" }
64
+ before do
65
+ subject.stub body: plaintext
66
+ key.stub(:encrypt).with(plaintext).and_return([ciphertext, skey])
67
+ Slosilo.stub(:[]).with(keyname).and_return key
68
+ end
69
+
70
+ it "encrypts the message body and adds the X-Slosilo-Key header" do
71
+ subject.should_receive(:body=).with ciphertext
72
+ subject.should_receive(:[]=).with 'X-Slosilo-Key', Base64::urlsafe_encode64(skey)
73
+ encrypt
74
+ end
75
+ end
76
+
77
+ context "when the body is empty" do
78
+ before { subject.stub body: "" }
79
+ it "doesn't set the key header" do
80
+ subject.should_not_receive(:[]=).with 'X-Slosilo-Key'
81
+ encrypt
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ describe "#exec" do
88
+ class Subject
89
+ def exec *a
90
+ "ok, got it"
91
+ end
92
+
93
+ def initialize keyname
94
+ extend Slosilo::HTTPRequest
95
+ self.keyname = keyname
96
+ end
97
+ end
98
+
99
+ subject { Subject.new keyname }
100
+
101
+ it "encrypts, then signs and delegates to the superclass" do
102
+ subject.should_receive(:encrypt!).once.ordered
103
+ subject.should_receive(:sign!).once.ordered
104
+ subject.exec(:foo).should == "ok, got it"
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe "http request stack" do
4
+ include_context "with example key"
5
+ include_context "with mock adapter"
6
+ before { Slosilo[:own] = key }
7
+
8
+ class MockRequest < Hash
9
+ def exec *a
10
+ end
11
+
12
+ def [] name
13
+ name = name.sub(/^HTTP_/,'').gsub('_', '-').split(/(\W)/).map(&:capitalize).join
14
+ result = super name
15
+ end
16
+
17
+ def initialize
18
+ extend Slosilo::HTTPRequest
19
+ self['Authorization'] = "Simon says it's fine"
20
+ end
21
+ end
22
+
23
+ subject { MockRequest.new }
24
+ let(:path) { '/some/path' }
25
+
26
+ context "with authorization header" do
27
+ it "works" do
28
+ mw = Slosilo::Rack::Middleware.new lambda{|_|:ok}, signature_required: true
29
+ subject.stub path: path, body: ''
30
+ mw.stub path: path
31
+ subject.send :exec
32
+ mw.call(subject).should == :ok
33
+ end
34
+
35
+ it "detects tampering" do
36
+ mw = Slosilo::Rack::Middleware.new lambda{|_|:ok}, signature_required: true
37
+ subject.stub path: path, body: ''
38
+ mw.stub path: path
39
+ subject.send :exec
40
+ subject['Authorization'] = "Simon changed his mind"
41
+ mw.call(subject).should_not == :ok
42
+ end
43
+ end
44
+ end