slosilo 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +10 -0
  3. data/.gitignore +21 -0
  4. data/.gitleaks.toml +221 -0
  5. data/.kateproject +4 -0
  6. data/CHANGELOG.md +50 -0
  7. data/CONTRIBUTING.md +16 -0
  8. data/Gemfile +4 -0
  9. data/Jenkinsfile +132 -0
  10. data/LICENSE +22 -0
  11. data/README.md +152 -0
  12. data/Rakefile +17 -0
  13. data/SECURITY.md +42 -0
  14. data/dev/Dockerfile.dev +7 -0
  15. data/dev/docker-compose.yml +8 -0
  16. data/lib/slosilo/adapters/abstract_adapter.rb +23 -0
  17. data/lib/slosilo/adapters/file_adapter.rb +42 -0
  18. data/lib/slosilo/adapters/memory_adapter.rb +31 -0
  19. data/lib/slosilo/adapters/mock_adapter.rb +21 -0
  20. data/lib/slosilo/adapters/sequel_adapter/migration.rb +52 -0
  21. data/lib/slosilo/adapters/sequel_adapter.rb +96 -0
  22. data/lib/slosilo/attr_encrypted.rb +85 -0
  23. data/lib/slosilo/errors.rb +15 -0
  24. data/lib/slosilo/jwt.rb +122 -0
  25. data/lib/slosilo/key.rb +218 -0
  26. data/lib/slosilo/keystore.rb +89 -0
  27. data/lib/slosilo/random.rb +11 -0
  28. data/lib/slosilo/symmetric.rb +63 -0
  29. data/lib/slosilo/version.rb +22 -0
  30. data/lib/slosilo.rb +13 -0
  31. data/lib/tasks/slosilo.rake +32 -0
  32. data/publish.sh +5 -0
  33. data/secrets.yml +1 -0
  34. data/slosilo.gemspec +38 -0
  35. data/spec/encrypted_attributes_spec.rb +114 -0
  36. data/spec/file_adapter_spec.rb +81 -0
  37. data/spec/jwt_spec.rb +102 -0
  38. data/spec/key_spec.rb +258 -0
  39. data/spec/keystore_spec.rb +26 -0
  40. data/spec/random_spec.rb +19 -0
  41. data/spec/sequel_adapter_spec.rb +171 -0
  42. data/spec/slosilo_spec.rb +124 -0
  43. data/spec/spec_helper.rb +84 -0
  44. data/spec/symmetric_spec.rb +94 -0
  45. data/test.sh +8 -0
  46. metadata +238 -0
data/SECURITY.md ADDED
@@ -0,0 +1,42 @@
1
+ # Security Policies and Procedures
2
+
3
+ This document outlines security procedures and general policies for the CyberArk Conjur
4
+ suite of tools and products.
5
+
6
+ * [Reporting a Bug](#reporting-a-bug)
7
+ * [Disclosure Policy](#disclosure-policy)
8
+ * [Comments on this Policy](#comments-on-this-policy)
9
+
10
+ ## Reporting a Bug
11
+
12
+ The CyberArk Conjur team and community take all security bugs in the Conjur suite seriously.
13
+ Thank you for improving the security of the Conjur suite. We appreciate your efforts and
14
+ responsible disclosure and will make every effort to acknowledge your
15
+ contributions.
16
+
17
+ Report security bugs by emailing the lead maintainers at security@conjur.org.
18
+
19
+ The maintainers will acknowledge your email within 2 business days. Subsequently, we will
20
+ send a more detailed response within 2 business days of our acknowledgement indicating
21
+ the next steps in handling your report. After the initial reply to your report, the security
22
+ team will endeavor to keep you informed of the progress towards a fix and full
23
+ announcement, and may ask for additional information or guidance.
24
+
25
+ Report security bugs in third-party modules to the person or team maintaining
26
+ the module.
27
+
28
+ ## Disclosure Policy
29
+
30
+ When the security team receives a security bug report, they will assign it to a
31
+ primary handler. This person will coordinate the fix and release process,
32
+ involving the following steps:
33
+
34
+ * Confirm the problem and determine the affected versions.
35
+ * Audit code to find any potential similar problems.
36
+ * Prepare fixes for all releases still under maintenance. These fixes will be
37
+ released as fast as possible.
38
+
39
+ ## Comments on this Policy
40
+
41
+ If you have suggestions on how this process could be improved please submit a
42
+ pull request.
@@ -0,0 +1,7 @@
1
+ FROM ruby
2
+
3
+ COPY ./ /src/
4
+
5
+ WORKDIR /src
6
+
7
+ RUN bundle
@@ -0,0 +1,8 @@
1
+ version: '3'
2
+ services:
3
+ dev:
4
+ build:
5
+ context: ..
6
+ dockerfile: dev/Dockerfile.dev
7
+ volumes:
8
+ - ../:/src
@@ -0,0 +1,23 @@
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 get_by_fingerprint fp
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def put_key id, key
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def each
19
+ raise NotImplementedError
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ require 'slosilo/adapters/abstract_adapter'
2
+
3
+ module Slosilo
4
+ module Adapters
5
+ class FileAdapter < AbstractAdapter
6
+ attr_reader :dir
7
+
8
+ def initialize(dir)
9
+ @dir = dir
10
+ @keys = {}
11
+ @fingerprints = {}
12
+ Dir[File.join(@dir, "*.key")].each do |f|
13
+ key = Slosilo::EncryptedAttributes.decrypt File.read(f)
14
+ id = File.basename(f, '.key')
15
+ key = @keys[id] = Slosilo::Key.new(key)
16
+ @fingerprints[key.fingerprint] = id
17
+ end
18
+ end
19
+
20
+ def put_key id, value
21
+ raise "id should not contain a period" if id.index('.')
22
+ fname = File.join(dir, "#{id}.key")
23
+ File.write(fname, Slosilo::EncryptedAttributes.encrypt(value.to_der))
24
+ File.chmod(0400, fname)
25
+ @keys[id] = value
26
+ end
27
+
28
+ def get_key id
29
+ @keys[id]
30
+ end
31
+
32
+ def get_by_fingerprint fp
33
+ id = @fingerprints[fp]
34
+ [@keys[id], id]
35
+ end
36
+
37
+ def each(&block)
38
+ @keys.each(&block)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ require 'slosilo/adapters/abstract_adapter'
2
+
3
+ module Slosilo
4
+ module Adapters
5
+ class MemoryAdapter < AbstractAdapter
6
+ def initialize
7
+ @keys = {}
8
+ @fingerprints = {}
9
+ end
10
+
11
+ def put_key id, key
12
+ key = Slosilo::Key.new(key) if key.is_a?(String)
13
+ @keys[id] = key
14
+ @fingerprints[key.fingerprint] = id
15
+ end
16
+
17
+ def get_key id
18
+ @keys[id]
19
+ end
20
+
21
+ def get_by_fingerprint fp
22
+ id = @fingerprints[fp]
23
+ [@keys[id], id]
24
+ end
25
+
26
+ def each(&block)
27
+ @keys.each(&block)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ module Slosilo
2
+ module Adapters
3
+ class MockAdapter < Hash
4
+ def initialize
5
+ @fp = {}
6
+ end
7
+
8
+ def put_key id, key
9
+ @fp[key.fingerprint] = id
10
+ self[id] = key
11
+ end
12
+
13
+ alias :get_key :[]
14
+
15
+ def get_by_fingerprint fp
16
+ id = @fp[fp]
17
+ [self[id], id]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,52 @@
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
+ # docs say to not use create_table? in migration;
19
+ # but we really want this to be robust in case there are any previous installs
20
+ # and we can't use table_exists? because it rolls back
21
+ create_table? keystore_table do
22
+ String :id, primary_key: true
23
+ bytea :key, null: false
24
+ String :fingerprint, unique: true, null: false
25
+ end
26
+ end
27
+
28
+ # Drop the table
29
+ def drop_keystore_table
30
+ drop_table keystore_table
31
+ end
32
+ end
33
+
34
+ module Extension
35
+ def slosilo_keystore
36
+ extend Slosilo::Adapters::SequelAdapter::Migration
37
+ end
38
+ end
39
+
40
+ Sequel::Database.send :include, Extension
41
+ end
42
+
43
+ Sequel.migration do
44
+ up do
45
+ slosilo_keystore
46
+ create_keystore_table
47
+ end
48
+ down do
49
+ slosilo_keystore
50
+ drop_keystore_table
51
+ end
52
+ end
@@ -0,0 +1,96 @@
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 secure?
11
+ !Slosilo.encryption_key.nil?
12
+ end
13
+
14
+ def create_model
15
+ model = Sequel::Model(:slosilo_keystore)
16
+ model.unrestrict_primary_key
17
+ model.attr_encrypted(:key, aad: :id) if secure?
18
+ model
19
+ end
20
+
21
+ def put_key id, value
22
+ fail Error::InsecureKeyStorage unless secure? || !value.private?
23
+
24
+ attrs = { id: id, key: value.to_der }
25
+ attrs[:fingerprint] = value.fingerprint if fingerprint_in_db?
26
+ model.create attrs
27
+ end
28
+
29
+ def get_key id
30
+ stored = model[id]
31
+ return nil unless stored
32
+ Slosilo::Key.new stored.key
33
+ end
34
+
35
+ def get_by_fingerprint fp
36
+ if fingerprint_in_db?
37
+ stored = model[fingerprint: fp]
38
+ return nil unless stored
39
+ [Slosilo::Key.new(stored.key), stored.id]
40
+ else
41
+ warn "Please migrate to a new database schema using rake slosilo:migrate for efficient fingerprint lookups"
42
+ find_by_fingerprint fp
43
+ end
44
+ end
45
+
46
+ def each
47
+ model.each do |m|
48
+ yield m.id, Slosilo::Key.new(m.key)
49
+ end
50
+ end
51
+
52
+ def recalculate_fingerprints
53
+ # Use a transaction to ensure that all fingerprints are updated together. If any update fails,
54
+ # we want to rollback all updates.
55
+ model.db.transaction do
56
+ model.each do |m|
57
+ m.update fingerprint: Slosilo::Key.new(m.key).fingerprint
58
+ end
59
+ end
60
+ end
61
+
62
+
63
+ def migrate!
64
+ unless fingerprint_in_db?
65
+ model.db.transaction do
66
+ model.db.alter_table :slosilo_keystore do
67
+ add_column :fingerprint, String
68
+ end
69
+
70
+ # reload the schema
71
+ model.set_dataset model.dataset
72
+
73
+ recalculate_fingerprints
74
+
75
+ model.db.alter_table :slosilo_keystore do
76
+ set_column_not_null :fingerprint
77
+ add_unique_constraint :fingerprint
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def fingerprint_in_db?
86
+ model.columns.include? :fingerprint
87
+ end
88
+
89
+ def find_by_fingerprint fp
90
+ each do |id, k|
91
+ return [k, id] if k.fingerprint == fp
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,85 @@
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
+
9
+ # @param options [Hash]
10
+ # @option :aad [#to_proc, #to_s] Provide additional authenticated data for
11
+ # encryption. This should be something unique to the instance having
12
+ # this attribute, such as a primary key; this will ensure that an attacker can't swap
13
+ # values around -- trying to decrypt value with a different auth data will fail.
14
+ # This means you have to be able to recover it in order to decrypt attributes.
15
+ # The following values are accepted:
16
+ #
17
+ # * Something proc-ish: will be called with self each time auth data is needed.
18
+ # * Something stringish: will be to_s-d and used for all instances as auth data.
19
+ # Note that this will only prevent swapping in data using another string.
20
+ #
21
+ # The recommended way to use this option is to pass a proc-ish that identifies the record.
22
+ # Note the proc-ish can be a simple method name; for example in case of a Sequel::Model:
23
+ # attr_encrypted :secret, aad: :pk
24
+ def attr_encrypted *a
25
+ options = a.last.is_a?(Hash) ? a.pop : {}
26
+ aad = options[:aad]
27
+ # note nil.to_s is "", which is exactly the right thing
28
+ auth_data = aad.respond_to?(:to_proc) ? aad.to_proc : proc{ |_| aad.to_s }
29
+
30
+ # In ruby 3 .arity for #proc returns both 1 and 2, depends on internal #proc
31
+ # This method is also being called with aad which is string, in such case the arity is 1
32
+ raise ":aad proc must take two arguments" unless (auth_data.arity.abs == 2 || auth_data.arity.abs == 1)
33
+
34
+ # push a module onto the inheritance hierarchy
35
+ # this allows calling super in classes
36
+ include(accessors = Module.new)
37
+ accessors.module_eval do
38
+ a.each do |attr|
39
+ define_method "#{attr}=" do |value|
40
+ super(EncryptedAttributes.encrypt(value, aad: auth_data[self]))
41
+ end
42
+ define_method attr do
43
+ EncryptedAttributes.decrypt(super(), aad: auth_data[self])
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ def self.included base
52
+ base.extend ClassMethods
53
+ end
54
+
55
+ class << self
56
+ def encrypt value, opts={}
57
+ return nil unless value
58
+ cipher.encrypt value, key: key, aad: opts[:aad]
59
+ end
60
+
61
+ def decrypt ctxt, opts={}
62
+ return nil unless ctxt
63
+ cipher.decrypt ctxt, key: key, aad: opts[:aad]
64
+ end
65
+
66
+ def key
67
+ Slosilo::encryption_key || (raise "Please set Slosilo::encryption_key")
68
+ end
69
+
70
+ def cipher
71
+ @cipher ||= Slosilo::Symmetric.new
72
+ end
73
+ end
74
+ end
75
+
76
+ class << self
77
+ attr_writer :encryption_key
78
+
79
+ def encryption_key
80
+ @encryption_key
81
+ end
82
+ end
83
+ end
84
+
85
+ Object.send :include, Slosilo::EncryptedAttributes
@@ -0,0 +1,15 @@
1
+ module Slosilo
2
+ class Error < RuntimeError
3
+ # An error thrown when attempting to store a private key in an unecrypted
4
+ # storage. Set Slosilo.encryption_key to secure the storage or make sure
5
+ # to store just the public keys (using Key#public).
6
+ class InsecureKeyStorage < Error
7
+ def initialize msg = "can't store a private key in a plaintext storage"
8
+ super
9
+ end
10
+ end
11
+
12
+ class TokenValidationError < Error
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,122 @@
1
+ require 'json'
2
+
3
+ module Slosilo
4
+ # A JWT-formatted Slosilo token.
5
+ # @note This is not intended to be a general-purpose JWT implementation.
6
+ class JWT
7
+ # Create a new unsigned token with the given claims.
8
+ # @param claims [#to_h] claims to embed in this token.
9
+ def initialize claims = {}
10
+ @claims = JSONHash[claims]
11
+ end
12
+
13
+ # Parse a token in compact representation
14
+ def self.parse_compact raw
15
+ load *raw.split('.', 3).map(&Base64.method(:urlsafe_decode64))
16
+ end
17
+
18
+ # Parse a token in JSON representation.
19
+ # @note only single signature is currently supported.
20
+ def self.parse_json raw
21
+ raw = JSON.load raw unless raw.respond_to? :to_h
22
+ parts = raw.to_h.values_at(*%w(protected payload signature))
23
+ fail ArgumentError, "input not a complete JWT" unless parts.all?
24
+ load *parts.map(&Base64.method(:urlsafe_decode64))
25
+ end
26
+
27
+ # Add a signature.
28
+ # @note currently only a single signature is handled;
29
+ # the token will be frozen after this operation.
30
+ def add_signature header, &sign
31
+ @claims = canonicalize_claims.freeze
32
+ @header = JSONHash[header].freeze
33
+ @signature = sign[string_to_sign].freeze
34
+ freeze
35
+ end
36
+
37
+ def string_to_sign
38
+ [header, claims].map(&method(:encode)).join '.'
39
+ end
40
+
41
+ # Returns the JSON serialization of this JWT.
42
+ def to_json *a
43
+ {
44
+ protected: encode(header),
45
+ payload: encode(claims),
46
+ signature: encode(signature)
47
+ }.to_json *a
48
+ end
49
+
50
+ # Returns the compact serialization of this JWT.
51
+ def to_s
52
+ [header, claims, signature].map(&method(:encode)).join('.')
53
+ end
54
+
55
+ attr_accessor :claims, :header, :signature
56
+
57
+ private
58
+
59
+ # Create a JWT token object from existing header, payload, and signature strings.
60
+ # @param header [#to_s] URLbase64-encoded representation of the protected header
61
+ # @param payload [#to_s] URLbase64-encoded representation of the token payload
62
+ # @param signature [#to_s] URLbase64-encoded representation of the signature
63
+ def self.load header, payload, signature
64
+ self.new(JSONHash.load payload).tap do |token|
65
+ token.header = JSONHash.load header
66
+ token.signature = signature.to_s.freeze
67
+ token.freeze
68
+ end
69
+ end
70
+
71
+ def canonicalize_claims
72
+ claims[:iat] = Time.now unless claims.include? :iat
73
+ claims[:iat] = claims[:iat].to_time.to_i
74
+ claims[:exp] = claims[:exp].to_time.to_i if claims.include? :exp
75
+ JSONHash[claims.to_a]
76
+ end
77
+
78
+ # Convenience method to make the above code clearer.
79
+ # Converts to string and urlbase64-encodes.
80
+ def encode s
81
+ Base64.urlsafe_encode64 s.to_s
82
+ end
83
+
84
+ # a hash with a possibly frozen JSON stringification
85
+ class JSONHash < Hash
86
+ def to_s
87
+ @repr || to_json
88
+ end
89
+
90
+ def freeze
91
+ @repr = to_json.freeze
92
+ super
93
+ end
94
+
95
+ def self.load raw
96
+ self[JSON.load raw.to_s].tap do |h|
97
+ h.send :repr=, raw
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def repr= raw
104
+ @repr = raw.freeze
105
+ freeze
106
+ end
107
+ end
108
+ end
109
+
110
+ # Try to convert by detecting token representation and parsing
111
+ def self.JWT raw
112
+ if raw.is_a? JWT
113
+ raw
114
+ elsif raw.respond_to?(:to_h) || raw =~ /\A\s*\{/
115
+ JWT.parse_json raw
116
+ else
117
+ JWT.parse_compact raw
118
+ end
119
+ rescue
120
+ raise ArgumentError, "invalid value for JWT(): #{raw.inspect}"
121
+ end
122
+ end