sidekiq-crypt 0.1.0 → 0.1.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27c186534a5ae125f231c97ecb3de4da4937a3a0e5a00dd4483e3d465f6ca597
4
- data.tar.gz: 2882b0a69fd17bbc49cf69807cdc0fe3eaf9f537fe99d5f79c70f32ef9d58c3c
3
+ metadata.gz: 2cf6eb93e1cb1c3a08d28e06e5bf7bcbe302af373377f0aca7ff316043a71a8d
4
+ data.tar.gz: d8d4a6c6f1aaee676069a00bff9a42e3784b7aff00004ca3c540e077aa258454
5
5
  SHA512:
6
- metadata.gz: 5cf5c266c766cd63279c3347daa0669fdfacf6e2cd48894d1280c41938dcd007ab64e96055cae60b002ad39b39a6955b682abe9bb555eb338deca7ddf0630901
7
- data.tar.gz: caa5b5233652856aca55e17332518dfc63d1ec1e2d323fd688bc4fca5bad309b106a2da16531705d3121d76429160b1907e69e8aee3bbd8f8850c53ed2169e28
6
+ metadata.gz: 93851ab0b7b7351a6532cdee053a5b2952acc4d67fe91db721f9558e458d20154eeac7512b12fdb73a74420d51250031017a60a87e22bc23c08dcdb4b299e76d
7
+ data.tar.gz: d5125e23edf014d8f2a80cd535bb7477d46fcbe2fcfafd288c4355c384662bafae1aa01342f94e2273eb9e7a630b9dcdc501403ec0cf2ebe33f65b1fd1ed49ed
data/lib/sidekiq-crypt.rb CHANGED
@@ -1,41 +1,73 @@
1
- require "sidekiq-crypt/version"
2
- require 'sidekiq-crypt/configuration'
3
- require 'sidekiq-crypt/client_middleware'
4
- require 'sidekiq-crypt/server_middleware'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'sidekiq-crypt/version'
4
+ require_relative 'sidekiq-crypt/configuration'
5
+ require_relative 'sidekiq-crypt/cipher'
6
+ require_relative 'sidekiq-crypt/client_middleware'
7
+ require_relative 'sidekiq-crypt/server_middleware'
8
+ require_relative 'sidekiq-crypt/worker'
5
9
 
6
10
  module Sidekiq
7
11
  module Crypt
8
12
  class << self
9
- def configuration
10
- @configuration ||= Configuration.new
13
+ def configuration(options = {})
14
+ @configuration ||= Configuration.new(options)
11
15
  end
12
16
 
13
17
  def configure(options = {})
14
- yield(configuration) if block_given?
18
+ block_given? ? yield(configuration(options)) : configuration(options)
15
19
 
16
20
  configuration.filters.flatten!
17
- raise 'you must specify at least one filter' if configuration.filters.empty?
21
+ validate_configuration!
18
22
 
19
23
  inject_sidekiq_middlewares
20
24
  end
21
25
 
22
26
  def inject_sidekiq_middlewares
27
+ inject_client_middleware
28
+ inject_server_middleware
29
+ end
30
+
31
+ private
32
+
33
+ def inject_client_middleware
23
34
  ::Sidekiq.configure_client do |config|
24
35
  config.client_middleware do |chain|
25
36
  chain.add Sidekiq::Crypt::ClientMiddleware, configuration: configuration
26
37
  end
27
38
  end
39
+ end
28
40
 
41
+ def inject_server_middleware
29
42
  ::Sidekiq.configure_server do |config|
30
43
  config.client_middleware do |chain|
31
44
  chain.add Sidekiq::Crypt::ClientMiddleware, configuration: configuration
32
45
  end
33
46
 
34
47
  config.server_middleware do |chain|
35
- chain.add Sidekiq::Crypt::ServerMiddleware, configuration: configuration
48
+ chain.add Sidekiq::Crypt::ServerMiddleware
36
49
  end
37
50
  end
38
51
  end
52
+
53
+ def validate_configuration!
54
+ raise 'you must specify current key version' if configuration.current_key_version.blank?
55
+ raise 'you must specify a hash for key store' if configuration.key_store.blank?
56
+ raise "current_key_version can't be found in key_store" if current_raw_key.nil?
57
+ raise 'current key is not valid for encryption' if invalid_key?
58
+ end
59
+
60
+ def current_raw_key
61
+ # returns the base64 encoded version of the key
62
+ configuration.key_store[configuration.current_key_version]
63
+ end
64
+
65
+ def invalid_key?
66
+ Sidekiq::Crypt::Cipher.encrypt('dummy_str', Sidekiq::Crypt::Cipher.random_iv)
67
+ false
68
+ rescue StandardError
69
+ true
70
+ end
39
71
  end
40
72
  end
41
73
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+
6
+ module Sidekiq
7
+ module Crypt
8
+ module Cipher
9
+ class << self
10
+ def encrypt(confidential_param, initialization_vector)
11
+ encryptor = encryption_cipher(initialization_vector)
12
+ # use base64 to prevent Encoding::UndefinedConversionError
13
+ Base64.encode64(encryptor.update(confidential_param.to_s) + encryptor.final)
14
+ end
15
+
16
+ def decrypt(confidential_param, initialization_vector, key_version)
17
+ decryptor_cipher = decryption_cipher(initialization_vector, key_version)
18
+
19
+ decryptor_cipher.update(Base64.decode64(confidential_param.to_s)) + decryptor_cipher.final
20
+ end
21
+
22
+ def random_iv
23
+ OpenSSL::Cipher::AES.new(256, :CBC).encrypt.random_iv
24
+ end
25
+
26
+ private
27
+
28
+ def encryption_cipher(initialization_vector)
29
+ cipher = OpenSSL::Cipher::AES.new(256, :CBC).encrypt
30
+ cipher.key = Sidekiq::Crypt.configuration.current_key
31
+ cipher.iv = initialization_vector
32
+
33
+ cipher
34
+ end
35
+
36
+ def decryption_cipher(initialization_vector, key_version)
37
+ cipher = OpenSSL::Cipher::AES.new(256, :CBC).decrypt
38
+ cipher.key = Sidekiq::Crypt.configuration.key_by_version(key_version)
39
+ cipher.iv = initialization_vector
40
+
41
+ cipher
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq-crypt/traverser'
4
+ require 'set'
5
+
6
+ module Sidekiq
7
+ module Crypt
8
+ class ClientMiddleware
9
+ def initialize(opts = {})
10
+ @configuration = opts[:configuration]
11
+ @encrypted_keys = Set.new
12
+ end
13
+
14
+ def call(worker_class, job, _queue, _redis_pool)
15
+ if encrypted_worker?(worker_class, job)
16
+ @iv = Cipher.random_iv
17
+ traverser.traverse!(job['args'], encryption_proc)
18
+ write_encryption_header_to_redis(job['jid'], encrypted_keys)
19
+ end
20
+
21
+ yield
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :configuration, :encrypted_keys
27
+
28
+ def encrypted_worker?(worker_class, job)
29
+ @worker_klass = worker_class(worker_class, job)
30
+ @worker_klass && @worker_klass.ancestors.include?(Sidekiq::Crypt::Worker)
31
+ end
32
+
33
+ def traverser
34
+ Traverser.new(@worker_klass.sidekiq_crypt_worker_filters || configuration.filters)
35
+ end
36
+
37
+ def write_encryption_header_to_redis(job_id, encrypted_keys)
38
+ Sidekiq.redis do |conn|
39
+ conn.set(
40
+ "sidekiq-crpyt-header:#{job_id}",
41
+ JSON.generate(
42
+ nonce: Base64.encode64(@iv),
43
+ encrypted_keys: encrypted_keys.to_a,
44
+ key_version: Sidekiq::Crypt.configuration.current_key_version
45
+ )
46
+ )
47
+ end
48
+ end
49
+
50
+ def encryption_proc
51
+ proc do |key, param|
52
+ encrypted_keys << key
53
+ Cipher.encrypt(param, @iv)
54
+ end
55
+ end
56
+
57
+ def worker_class(worker_class, job)
58
+ klass = begin
59
+ job['args'][0]['job_class'] || worker_class
60
+ rescue StandardError
61
+ worker_class
62
+ end
63
+
64
+ if klass.is_a?(Class)
65
+ klass
66
+ elsif Module.const_defined?(klass)
67
+ Module.const_get(klass)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Crypt
5
+ class Configuration
6
+ attr_accessor :filters, :current_key_version
7
+ attr_reader :key_store
8
+
9
+ def initialize(options = {})
10
+ @filters = []
11
+ @current_key_version = options.fetch(:current_key_version, nil)
12
+ @key_store = options.fetch(:key_store, {}).transform_keys(&:to_s)
13
+
14
+ include_rails_filter_parameters(options[:exclude_rails_filters])
15
+ end
16
+
17
+ def current_key
18
+ Base64.strict_decode64(key_store[current_key_version])
19
+ end
20
+
21
+ def key_by_version(given_key)
22
+ Base64.strict_decode64(key_store[given_key])
23
+ end
24
+
25
+ def key_store=(key_store_hash)
26
+ @key_store = (key_store_hash || {}).transform_keys(&:to_s)
27
+ end
28
+
29
+ private
30
+
31
+ def include_rails_filter_parameters(exclude_rails_filters)
32
+ return unless defined?(::Rails)
33
+ return if exclude_rails_filters
34
+
35
+ @filters = ::Rails.application.config.filter_parameters
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Crypt
5
+ class ServerMiddleware
6
+ FILTERED = '[FILTERED]'
7
+
8
+ def call(_worker_class, job, _queue, _redis_pool = {})
9
+ return yield unless encrypted_worker?(job)
10
+
11
+ encrypt_secret_params(job)
12
+
13
+ yield
14
+
15
+ delete_encryption_header(job['jid'])
16
+ rescue StandardError => e
17
+ if encrypted_worker?(job)
18
+ Traverser.new(@encryption_header[:encrypted_keys]).traverse!(job['args'], filter_proc)
19
+ end
20
+
21
+ raise e
22
+ end
23
+
24
+ private
25
+
26
+ def encrypt_secret_params(job)
27
+ @encryption_header = read_encryption_header_from_redis(job['jid'])
28
+ Traverser.new(@encryption_header[:encrypted_keys]).traverse!(job['args'], decryption_proc)
29
+ end
30
+
31
+ def encrypted_worker?(job)
32
+ klass = worker_klass(job)
33
+ klass && klass.ancestors.include?(Sidekiq::Crypt::Worker)
34
+ end
35
+
36
+ def worker_klass(job)
37
+ klass = begin
38
+ job['args'][0]['job_class'] || job['class']
39
+ rescue StandardError
40
+ job['class']
41
+ end
42
+ klass.is_a?(Class) ? klass : Module.const_get(klass)
43
+ end
44
+
45
+ def decryption_proc
46
+ proc do |_key, param|
47
+ Cipher.decrypt(param, @encryption_header[:iv], @encryption_header[:key_version])
48
+ end
49
+ end
50
+
51
+ def filter_proc
52
+ proc { |_key, _param| FILTERED }
53
+ end
54
+
55
+ def read_encryption_header_from_redis(job_id)
56
+ parsed_header = JSON.parse(read_encryption_header(job_id))
57
+
58
+ {
59
+ iv: Base64.decode64(parsed_header['nonce']),
60
+ encrypted_keys: parsed_header['encrypted_keys'],
61
+ key_version: parsed_header['key_version']
62
+ }
63
+ end
64
+
65
+ def read_encryption_header(job_id)
66
+ Sidekiq.redis do |conn|
67
+ conn.get("sidekiq-crpyt-header:#{job_id}")
68
+ end
69
+ end
70
+
71
+ def delete_encryption_header(job_id)
72
+ Sidekiq.redis do |conn|
73
+ conn.del("sidekiq-crpyt-header:#{job_id}")
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Sidekiq
6
+ module Crypt
7
+ class Traverser
8
+ def initialize(filters)
9
+ @filters = filters
10
+ end
11
+
12
+ def traverse!(args, proc)
13
+ args.each_with_index do |arg, index|
14
+ next unless arg.is_a?(Hash) || arg.is_a?(Array)
15
+
16
+ # override the params
17
+ args[index] = traverse(arg, proc)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def traverse(object, proc)
24
+ # sidekiq arguments must be serialized as JSON.
25
+ # Therefore, object could be hash, array or primitives like string/number
26
+ # Also, recursive hashes or arrays is not possible.
27
+ case object
28
+ when Hash
29
+ clean_hash = {}
30
+
31
+ object.each do |key, value|
32
+ clean_hash[key] = filter_match?(key) ? proc.call(key, value) : traverse(value, proc)
33
+ end
34
+
35
+ clean_hash
36
+ when Array then object.map { |element| traverse(element, proc) }
37
+ else object
38
+ end
39
+ end
40
+
41
+ def filter_match?(key)
42
+ @filters.any? do |filter|
43
+ case filter
44
+ when Regexp then key.to_s.match(filter)
45
+ else key.to_s == filter.to_s
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Crypt
5
+ VERSION = '0.1.1'
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Crypt
5
+ module Worker
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ attr_reader :sidekiq_crypt_worker_filters
12
+
13
+ def encrypted_keys(*filter_keys)
14
+ @sidekiq_crypt_worker_filters = filter_keys
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-crypt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Murat Toygar
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-10-06 00:00:00.000000000 Z
11
+ date: 2020-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
19
+ version: '5.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '3.0'
26
+ version: '5.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -39,35 +39,35 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.17'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rake
42
+ name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '10.0'
47
+ version: '5.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '10.0'
54
+ version: '5.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: minitest
56
+ name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '5.0'
61
+ version: '13.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '5.0'
68
+ version: '13.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: appraisal
70
+ name: rubocop
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -88,6 +88,13 @@ extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
90
  - lib/sidekiq-crypt.rb
91
+ - lib/sidekiq-crypt/cipher.rb
92
+ - lib/sidekiq-crypt/client_middleware.rb
93
+ - lib/sidekiq-crypt/configuration.rb
94
+ - lib/sidekiq-crypt/server_middleware.rb
95
+ - lib/sidekiq-crypt/traverser.rb
96
+ - lib/sidekiq-crypt/version.rb
97
+ - lib/sidekiq-crypt/worker.rb
91
98
  homepage: https://rubygems.org/gems/sidekiq-crypt
92
99
  licenses:
93
100
  - MIT
@@ -107,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
114
  - !ruby/object:Gem::Version
108
115
  version: '0'
109
116
  requirements: []
110
- rubygems_version: 3.0.4
117
+ rubygems_version: 3.0.6
111
118
  signing_key:
112
119
  specification_version: 4
113
120
  summary: encrypts confidential sidekiq parameters on redis