slosilo 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .rvmrc
19
+ .project
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in slosilo.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Rafał Rzepecki
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Slosilo
2
+
3
+ Slosilo is a keystore in the database. (Currently only works with postgres.)
4
+ It allows easy storage and retrieval of keys.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'slosilo'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Add a migration to create the necessary table:
17
+
18
+ require 'slosilo/adapters/sequel_adapter/migration'
19
+
20
+ Remember to migrate your database
21
+
22
+ $ rake db:migrate
23
+
24
+ ## Usage
25
+
26
+ ## Contributing
27
+
28
+ 1. Fork it
29
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
30
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
31
+ 4. Push to the branch (`git push origin my-new-feature`)
32
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ begin
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ rescue LoadError
8
+ $stderr.puts "RSpec Rake tasks not available in environment #{ENV['RACK_ENV']}"
9
+ end
10
+
11
+ task :jenkins do
12
+ require 'ci/reporter/rake/rspec'
13
+ Rake::Task["ci:setup:rspec"].invoke
14
+ Rake::Task["spec"].invoke
15
+ end
16
+
17
+ task :default => :spec
@@ -0,0 +1,19 @@
1
+ require 'slosilo/attr_encrypted'
2
+
3
+ module Slosilo
4
+ module Adapters
5
+ class AbstractAdapter
6
+ def get_key id
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def put_key id, key
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def each
15
+ raise NotImplementedError
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ module Slosilo
2
+ module Adapters
3
+ class MockAdapter < Hash
4
+ alias :put_key :[]=
5
+ alias :get_key :[]
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,49 @@
1
+ require 'sequel'
2
+
3
+ module Slosilo
4
+ module Adapters::SequelAdapter::Migration
5
+ # The default name of the table to hold the keys
6
+ DEFAULT_KEYSTORE_TABLE = :slosilo_keystore
7
+
8
+ # Sets up default keystore table name
9
+ def self.extended(db)
10
+ db.keystore_table ||= DEFAULT_KEYSTORE_TABLE
11
+ end
12
+
13
+ # Keystore table name. If changing this do it immediately after loading the extension.
14
+ attr_accessor :keystore_table
15
+
16
+ # Create the table for holding keys
17
+ def create_keystore_table
18
+ create_table keystore_table do
19
+ String :id, primary_key: true
20
+ # Note: currently only postgres is supported
21
+ bytea :key, null: false
22
+ end
23
+ end
24
+
25
+ # Drop the table
26
+ def drop_keystore_table
27
+ drop_table keystore_table
28
+ end
29
+ end
30
+
31
+ module Extension
32
+ def slosilo_keystore
33
+ extend Slosilo::Adapters::SequelAdapter::Migration
34
+ end
35
+ end
36
+
37
+ Sequel::Database.send :include, Extension
38
+ end
39
+
40
+ Sequel.migration do
41
+ up do
42
+ slosilo_keystore
43
+ create_keystore_table
44
+ end
45
+ down do
46
+ slosilo_keystore
47
+ drop_keystore_table
48
+ end
49
+ end
@@ -0,0 +1,34 @@
1
+ require 'slosilo/adapters/abstract_adapter'
2
+
3
+ module Slosilo
4
+ module Adapters
5
+ class SequelAdapter < AbstractAdapter
6
+ def model
7
+ @model ||= create_model
8
+ end
9
+
10
+ def create_model
11
+ model = Sequel::Model(:slosilo_keystore)
12
+ model.unrestrict_primary_key
13
+ model.attr_encrypted :key
14
+ model
15
+ end
16
+
17
+ def put_key id, value
18
+ model.create id: id, key: value
19
+ end
20
+
21
+ def get_key id
22
+ stored = model[id]
23
+ return nil unless stored
24
+ stored.key
25
+ end
26
+
27
+ def each
28
+ model.each do |m|
29
+ yield m.id, m.key
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,59 @@
1
+ require 'slosilo/symmetric'
2
+
3
+ module Slosilo
4
+ # we don't trust the database to keep all backups safe from the prying eyes
5
+ # so we encrypt sensitive attributes before storing them
6
+ module EncryptedAttributes
7
+ module ClassMethods
8
+ def attr_encrypted *a
9
+ # push a module onto the inheritance hierarchy
10
+ # this allows calling super in classes
11
+ include(accessors = Module.new)
12
+ accessors.module_eval do
13
+ a.each do |attr|
14
+ define_method "#{attr}=" do |value|
15
+ super(EncryptedAttributes.encrypt value)
16
+ end
17
+ define_method attr do
18
+ EncryptedAttributes.decrypt(super())
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.included base
26
+ base.extend ClassMethods
27
+ end
28
+
29
+ class << self
30
+ def encrypt value
31
+ return nil unless value
32
+ cipher.encrypt value, key: key
33
+ end
34
+
35
+ def decrypt ctxt
36
+ return nil unless ctxt
37
+ cipher.decrypt ctxt, key: key
38
+ end
39
+
40
+ def key
41
+ Slosilo::encryption_key || (raise "Please set Slosilo::encryption_key")
42
+ end
43
+
44
+ def cipher
45
+ @cipher ||= Slosilo::Symmetric.new
46
+ end
47
+ end
48
+ end
49
+
50
+ class << self
51
+ attr_writer :encryption_key
52
+
53
+ def encryption_key
54
+ @encryption_key
55
+ end
56
+ end
57
+ end
58
+
59
+ Object.send:include, Slosilo::EncryptedAttributes
@@ -0,0 +1,59 @@
1
+ module Slosilo
2
+ # A mixin module which simplifies generating signed and encrypted requests.
3
+ # It's designed to be mixed into a standard Net::HTTPRequest object
4
+ # and ensures the request is signed and optionally encrypted before execution.
5
+ # Requests prepared this way will be recognized by Slosilo::Rack::Middleware.
6
+ #
7
+ # As an example, you can use it with RestClient like so:
8
+ # RestClient.add_before_execution_proc do |req, params|
9
+ # require 'slosilo'
10
+ # req.extend Slosilo::HTTPRequest
11
+ # req.keyname = :somekey
12
+ # end
13
+ #
14
+ # The request won't be encrypted unless you set the destination keyname.
15
+
16
+ module HTTPRequest
17
+ # Encrypt the request with key named @keyname from Slosilo::Keystore.
18
+ # If calling this manually, make sure to encrypt before signing.
19
+ def encrypt!
20
+ return unless @keyname
21
+ return unless body && !body.empty?
22
+ self.body, key = Slosilo[@keyname].encrypt body
23
+ self['X-Slosilo-Key'] = Base64::urlsafe_encode64 key
24
+ end
25
+
26
+ # Sign the request with :own key from Slosilo::Keystore.
27
+ # If calling this manually, make sure to encrypt before signing.
28
+ def sign!
29
+ token = Slosilo[:own].signed_token signed_data
30
+ self['Timestamp'] = token["timestamp"]
31
+ self['X-Slosilo-Signature'] = token["signature"]
32
+ end
33
+
34
+ # Build the data hash to sign.
35
+ def signed_data
36
+ data = { "path" => path, "body" => [body].pack('m0') }
37
+ if key = self['X-Slosilo-Key']
38
+ data["key"] = key
39
+ end
40
+ if authz = self['Authorization']
41
+ data["authorization"] = authz
42
+ end
43
+ data
44
+ end
45
+
46
+ # Encrypt, sign and execute the request.
47
+ def exec *a
48
+ # we need to hook here because the body might be set
49
+ # in several ways and here it's hopefully finalized
50
+ encrypt!
51
+ sign!
52
+ super *a
53
+ end
54
+
55
+ # Name of the key used to encrypt the request.
56
+ # Use it to establish the identity of the receiver.
57
+ attr_accessor :keyname
58
+ end
59
+ end
@@ -0,0 +1,95 @@
1
+ require 'openssl'
2
+ require 'json'
3
+ require 'base64'
4
+ require 'time'
5
+
6
+ module Slosilo
7
+ class Key
8
+ def initialize raw_key = nil
9
+ @key = if raw_key.is_a? OpenSSL::PKey::RSA
10
+ raw_key
11
+ elsif !raw_key.nil?
12
+ OpenSSL::PKey.read raw_key
13
+ else
14
+ OpenSSL::PKey::RSA.new 2048
15
+ end
16
+ end
17
+
18
+ attr_reader :key
19
+
20
+ def cipher
21
+ @cipher ||= Slosilo::Symmetric.new
22
+ end
23
+
24
+ def encrypt plaintext
25
+ key = cipher.random_key
26
+ ctxt = cipher.encrypt plaintext, key: key
27
+ key = @key.public_encrypt key
28
+ [ctxt, key]
29
+ end
30
+
31
+ def decrypt ciphertext, skey
32
+ key = @key.private_decrypt skey
33
+ cipher.decrypt ciphertext, key: key
34
+ end
35
+
36
+ def to_s
37
+ @key.public_key.to_pem
38
+ end
39
+
40
+ def to_der
41
+ @key.to_der
42
+ end
43
+
44
+ def sign value
45
+ sign_string(stringify value)
46
+ end
47
+
48
+ SIGNATURE_LEN = 256
49
+
50
+ def verify_signature data, signature
51
+ signature, salt = signature.unpack("a#{SIGNATURE_LEN}a*")
52
+ key.public_decrypt(signature) == hash_function.digest(salt + stringify(data))
53
+ rescue
54
+ false
55
+ end
56
+
57
+ # create a new timestamped and signed token carrying data
58
+ def signed_token data
59
+ token = { "data" => data, "timestamp" => Time.new.utc.to_s }
60
+ token["signature"] = Base64::urlsafe_encode64(sign token)
61
+ token
62
+ end
63
+
64
+ def token_valid? token, expiry = 8 * 60
65
+ token = token.clone
66
+ signature = Base64::urlsafe_decode64(token.delete "signature")
67
+ (Time.parse(token["timestamp"]) + expiry > Time.now) && verify_signature(token, signature)
68
+ end
69
+
70
+ def sign_string value
71
+ _salt = salt
72
+ key.private_encrypt(hash_function.digest(_salt + value)) + _salt
73
+ end
74
+
75
+ private
76
+ def stringify value
77
+ case value
78
+ when Hash
79
+ value.to_a.sort.to_json
80
+ when String
81
+ value
82
+ else
83
+ value.to_json
84
+ end
85
+ end
86
+
87
+ def salt
88
+ Slosilo::Random::salt
89
+ end
90
+
91
+ def hash_function
92
+ @hash_function ||= OpenSSL::Digest::SHA256
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,61 @@
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
+ adapter.put_key id.to_s, key.to_der
11
+ end
12
+
13
+ def get id
14
+ key = adapter.get_key(id.to_s)
15
+ key && Key.new(key)
16
+ end
17
+
18
+ def each(&block)
19
+ adapter.each(&block)
20
+ end
21
+
22
+ def any? &block
23
+ catch :found do
24
+ adapter.each do |id, k|
25
+ throw :found if block.call(Key.new(k))
26
+ end
27
+ return false
28
+ end
29
+ true
30
+ end
31
+ end
32
+
33
+ class << self
34
+ def []= id, value
35
+ keystore.put id, value
36
+ end
37
+
38
+ def [] id
39
+ keystore.get id
40
+ end
41
+
42
+ def each(&block)
43
+ keystore.each(&block)
44
+ end
45
+
46
+ def sign object
47
+ self[:own].sign object
48
+ end
49
+
50
+ def token_valid? token
51
+ keystore.any? { |k| k.token_valid? token }
52
+ end
53
+
54
+ attr_accessor :adapter
55
+
56
+ private
57
+ def keystore
58
+ @keystore ||= Keystore.new
59
+ end
60
+ end
61
+ end
@@ -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
@@ -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,33 @@
1
+ module Slosilo
2
+ class Symmetric
3
+ def initialize
4
+ @cipher = OpenSSL::Cipher.new 'AES-256-CBC'
5
+ end
6
+
7
+ def encrypt plaintext, opts = {}
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
14
+ end
15
+
16
+ def decrypt ciphertext, opts = {}
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
23
+ end
24
+
25
+ def random_iv
26
+ @cipher.random_iv
27
+ end
28
+
29
+ def random_key
30
+ @cipher.random_key
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Slosilo
2
+ VERSION = "0.1.2"
3
+ end
data/lib/slosilo.rb ADDED
@@ -0,0 +1,13 @@
1
+ require "slosilo/version"
2
+ require "slosilo/keystore"
3
+ require "slosilo/symmetric"
4
+ require "slosilo/attr_encrypted"
5
+ require "slosilo/random"
6
+ require "slosilo/rack/middleware"
7
+ require "slosilo/http_request"
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)