slosilo 0.1.2

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.
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)