slosilo 0.0.0

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