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 +4 -4
- data/lib/sidekiq-crypt.rb +41 -9
- data/lib/sidekiq-crypt/cipher.rb +46 -0
- data/lib/sidekiq-crypt/client_middleware.rb +72 -0
- data/lib/sidekiq-crypt/configuration.rb +39 -0
- data/lib/sidekiq-crypt/server_middleware.rb +78 -0
- data/lib/sidekiq-crypt/traverser.rb +51 -0
- data/lib/sidekiq-crypt/version.rb +7 -0
- data/lib/sidekiq-crypt/worker.rb +19 -0
- metadata +21 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2cf6eb93e1cb1c3a08d28e06e5bf7bcbe302af373377f0aca7ff316043a71a8d
|
4
|
+
data.tar.gz: d8d4a6c6f1aaee676069a00bff9a42e3784b7aff00004ca3c540e077aa258454
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93851ab0b7b7351a6532cdee053a5b2952acc4d67fe91db721f9558e458d20154eeac7512b12fdb73a74420d51250031017a60a87e22bc23c08dcdb4b299e76d
|
7
|
+
data.tar.gz: d5125e23edf014d8f2a80cd535bb7477d46fcbe2fcfafd288c4355c384662bafae1aa01342f94e2273eb9e7a630b9dcdc501403ec0cf2ebe33f65b1fd1ed49ed
|
data/lib/sidekiq-crypt.rb
CHANGED
@@ -1,41 +1,73 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
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)
|
18
|
+
block_given? ? yield(configuration(options)) : configuration(options)
|
15
19
|
|
16
20
|
configuration.filters.flatten!
|
17
|
-
|
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
|
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,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.
|
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:
|
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: '
|
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: '
|
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:
|
42
|
+
name: minitest
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
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: '
|
54
|
+
version: '5.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
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: '
|
68
|
+
version: '13.0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
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.
|
117
|
+
rubygems_version: 3.0.6
|
111
118
|
signing_key:
|
112
119
|
specification_version: 4
|
113
120
|
summary: encrypts confidential sidekiq parameters on redis
|