chamber 2.10.2 → 2.11.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/LICENSE.txt +1 -1
- data/lib/chamber.rb +3 -6
- data/lib/chamber/binary/heroku.rb +1 -0
- data/lib/chamber/binary/runner.rb +29 -13
- data/lib/chamber/binary/travis.rb +1 -0
- data/lib/chamber/commands/base.rb +10 -9
- data/lib/chamber/commands/comparable.rb +1 -0
- data/lib/chamber/commands/compare.rb +8 -7
- data/lib/chamber/commands/files.rb +1 -0
- data/lib/chamber/commands/heroku.rb +1 -0
- data/lib/chamber/commands/heroku/clear.rb +2 -1
- data/lib/chamber/commands/heroku/compare.rb +1 -0
- data/lib/chamber/commands/heroku/pull.rb +3 -4
- data/lib/chamber/commands/heroku/push.rb +1 -0
- data/lib/chamber/commands/initialize.rb +88 -76
- data/lib/chamber/commands/securable.rb +3 -2
- data/lib/chamber/commands/secure.rb +3 -1
- data/lib/chamber/commands/show.rb +7 -6
- data/lib/chamber/commands/travis.rb +1 -0
- data/lib/chamber/commands/travis/secure.rb +1 -0
- data/lib/chamber/configuration.rb +14 -13
- data/lib/chamber/context_resolver.rb +52 -55
- data/lib/chamber/encryption_methods/none.rb +4 -2
- data/lib/chamber/encryption_methods/public_key.rb +4 -2
- data/lib/chamber/encryption_methods/ssl.rb +11 -9
- data/lib/chamber/errors/decryption_failure.rb +1 -0
- data/lib/chamber/file.rb +27 -18
- data/lib/chamber/file_set.rb +14 -13
- data/lib/chamber/filters/decryption_filter.rb +48 -18
- data/lib/chamber/filters/encryption_filter.rb +32 -22
- data/lib/chamber/filters/environment_filter.rb +109 -16
- data/lib/chamber/filters/failed_decryption_filter.rb +10 -8
- data/lib/chamber/filters/insecure_filter.rb +1 -0
- data/lib/chamber/filters/namespace_filter.rb +8 -7
- data/lib/chamber/filters/secure_filter.rb +10 -9
- data/lib/chamber/filters/translate_secure_keys_filter.rb +10 -9
- data/lib/chamber/instance.rb +5 -4
- data/lib/chamber/key_pair.rb +82 -0
- data/lib/chamber/keys/base.rb +64 -0
- data/lib/chamber/keys/decryption.rb +41 -0
- data/lib/chamber/keys/encryption.rb +41 -0
- data/lib/chamber/namespace_set.rb +10 -9
- data/lib/chamber/rails.rb +1 -0
- data/lib/chamber/rails/railtie.rb +1 -0
- data/lib/chamber/rubinius_fix.rb +1 -0
- data/lib/chamber/settings.rb +45 -41
- data/lib/chamber/types/secured.rb +14 -12
- data/lib/chamber/version.rb +2 -1
- metadata +28 -27
- metadata.gz.sig +0 -0
- data/lib/chamber/decryption_key.rb +0 -52
- data/lib/chamber/environmentable.rb +0 -27
- data/lib/chamber/filters/boolean_conversion_filter.rb +0 -41
data/lib/chamber/file_set.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'pathname'
|
3
4
|
require 'chamber/namespace_set'
|
4
5
|
require 'chamber/file'
|
@@ -111,11 +112,16 @@ require 'chamber/settings'
|
|
111
112
|
#
|
112
113
|
module Chamber
|
113
114
|
class FileSet
|
115
|
+
attr_reader :namespaces,
|
116
|
+
:paths
|
117
|
+
attr_accessor :decryption_keys,
|
118
|
+
:encryption_keys
|
119
|
+
|
114
120
|
def initialize(options = {})
|
115
|
-
self.namespaces
|
116
|
-
self.
|
117
|
-
self.
|
118
|
-
self.paths
|
121
|
+
self.namespaces = options[:namespaces] || {}
|
122
|
+
self.decryption_keys = options[:decryption_keys]
|
123
|
+
self.encryption_keys = options[:encryption_keys]
|
124
|
+
self.paths = options.fetch(:files)
|
119
125
|
end
|
120
126
|
|
121
127
|
###
|
@@ -177,11 +183,6 @@ class FileSet
|
|
177
183
|
|
178
184
|
protected
|
179
185
|
|
180
|
-
attr_reader :namespaces,
|
181
|
-
:paths
|
182
|
-
attr_accessor :decryption_key,
|
183
|
-
:encryption_key
|
184
|
-
|
185
186
|
###
|
186
187
|
# Internal: Allows the paths for the FileSet to be set. It can either be an
|
187
188
|
# object that responds to `#each` like an Array or one that doesn't. In which
|
@@ -216,10 +217,10 @@ class FileSet
|
|
216
217
|
relevant_glob_files = relevant_files & current_glob_files
|
217
218
|
|
218
219
|
relevant_glob_files.map! do |file|
|
219
|
-
File.new(path:
|
220
|
-
namespaces:
|
221
|
-
|
222
|
-
|
220
|
+
File.new(path: file,
|
221
|
+
namespaces: namespaces,
|
222
|
+
decryption_keys: decryption_keys,
|
223
|
+
encryption_keys: encryption_keys)
|
223
224
|
end
|
224
225
|
|
225
226
|
sorted_relevant_files += relevant_glob_files
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'openssl'
|
3
4
|
require 'base64'
|
4
5
|
require 'hashie/mash'
|
@@ -11,13 +12,31 @@ require 'chamber/errors/decryption_failure'
|
|
11
12
|
module Chamber
|
12
13
|
module Filters
|
13
14
|
class DecryptionFilter
|
14
|
-
SECURE_KEY_TOKEN = /\A_secure_/
|
15
15
|
BASE64_STRING_PATTERN = %r{\A[A-Za-z0-9\+/]{342}==\z}
|
16
|
-
LARGE_DATA_STRING_PATTERN = %r{
|
16
|
+
LARGE_DATA_STRING_PATTERN = %r{
|
17
|
+
\A # Beginning of String
|
18
|
+
(
|
19
|
+
[A-Za-z0-9\+\/#]*\={0,2} # Base64 Encoded Key
|
20
|
+
)
|
21
|
+
\# # Separator
|
22
|
+
(
|
23
|
+
[A-Za-z0-9\+\/#]*\={0,2} # Base64 Encoded IV
|
24
|
+
)
|
25
|
+
\# # Separator
|
26
|
+
(
|
27
|
+
[A-Za-z0-9\+\/#]*\={0,2} # Base64 Encoded Data
|
28
|
+
)
|
29
|
+
\z # End of String
|
30
|
+
}x
|
31
|
+
|
32
|
+
attr_accessor :data,
|
33
|
+
:secure_key_token
|
34
|
+
attr_reader :decryption_keys
|
17
35
|
|
18
36
|
def initialize(options = {})
|
19
|
-
self.
|
20
|
-
self.data
|
37
|
+
self.decryption_keys = options.fetch(:decryption_keys, {}) || {}
|
38
|
+
self.data = options.fetch(:data).dup
|
39
|
+
self.secure_key_token = /\A#{Regexp.escape(options.fetch(:secure_key_prefix))}/
|
21
40
|
end
|
22
41
|
|
23
42
|
def self.execute(options = {})
|
@@ -26,17 +45,14 @@ class DecryptionFilter
|
|
26
45
|
|
27
46
|
protected
|
28
47
|
|
29
|
-
attr_accessor :data
|
30
|
-
attr_reader :decryption_key
|
31
|
-
|
32
48
|
def execute(raw_data = data)
|
33
49
|
settings = Hashie::Mash.new
|
34
50
|
|
35
51
|
raw_data.each_pair do |key, value|
|
36
52
|
settings[key] = if value.respond_to? :each_pair
|
37
53
|
execute(value)
|
38
|
-
elsif key.match(
|
39
|
-
|
54
|
+
elsif key.match(secure_key_token)
|
55
|
+
decrypt(key, value)
|
40
56
|
else
|
41
57
|
value
|
42
58
|
end
|
@@ -45,20 +61,34 @@ class DecryptionFilter
|
|
45
61
|
settings
|
46
62
|
end
|
47
63
|
|
48
|
-
def
|
49
|
-
|
64
|
+
def decryption_keys=(other)
|
65
|
+
@decryption_keys = other.each_value.map do |keyish|
|
66
|
+
content = if ::File.readable?(::File.expand_path(keyish))
|
67
|
+
::File.read(::File.expand_path(keyish))
|
68
|
+
else
|
69
|
+
keyish
|
70
|
+
end
|
50
71
|
|
51
|
-
|
52
|
-
|
53
|
-
else
|
54
|
-
keyish
|
55
|
-
end
|
56
|
-
|
57
|
-
@decryption_key = OpenSSL::PKey::RSA.new(key_content)
|
72
|
+
OpenSSL::PKey::RSA.new(content)
|
73
|
+
end
|
58
74
|
end
|
59
75
|
|
60
76
|
private
|
61
77
|
|
78
|
+
def decrypt(key, value)
|
79
|
+
method = decryption_method(value)
|
80
|
+
|
81
|
+
decryption_keys.each do |decryption_key|
|
82
|
+
begin
|
83
|
+
return method.decrypt(key, value, decryption_key)
|
84
|
+
rescue OpenSSL::PKey::RSAError
|
85
|
+
next
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
value
|
90
|
+
end
|
91
|
+
|
62
92
|
def decryption_method(value)
|
63
93
|
if value.respond_to?(:match)
|
64
94
|
if value.match(BASE64_STRING_PATTERN)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'openssl'
|
3
4
|
require 'base64'
|
4
5
|
require 'hashie/mash'
|
@@ -10,13 +11,17 @@ require 'chamber/encryption_methods/none'
|
|
10
11
|
module Chamber
|
11
12
|
module Filters
|
12
13
|
class EncryptionFilter
|
13
|
-
SECURE_KEY_TOKEN = /\A_secure_/
|
14
14
|
BASE64_STRING_PATTERN = %r{\A[A-Za-z0-9\+\/]{342}==\z}
|
15
15
|
LARGE_DATA_STRING_PATTERN = %r{\A([A-Za-z0-9\+\/#]*\={0,2})#([A-Za-z0-9\+\/#]*\={0,2})#([A-Za-z0-9\+\/#]*\={0,2})\z} # rubocop:disable Metrics/LineLength
|
16
16
|
|
17
|
+
attr_accessor :data,
|
18
|
+
:secure_key_token
|
19
|
+
attr_reader :encryption_keys
|
20
|
+
|
17
21
|
def initialize(options = {})
|
18
|
-
self.
|
19
|
-
self.data
|
22
|
+
self.encryption_keys = options.fetch(:encryption_keys, {}) || {}
|
23
|
+
self.data = options.fetch(:data).dup
|
24
|
+
self.secure_key_token = /\A#{Regexp.escape(options.fetch(:secure_key_prefix))}/
|
20
25
|
end
|
21
26
|
|
22
27
|
def self.execute(options = {})
|
@@ -25,35 +30,40 @@ class EncryptionFilter
|
|
25
30
|
|
26
31
|
protected
|
27
32
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def execute(raw_data = data)
|
32
|
-
settings = Hashie::Mash.new
|
33
|
-
|
34
|
-
raw_data.each_pair do |key, value|
|
33
|
+
def execute(raw_data = data, namespace = nil)
|
34
|
+
raw_data.each_with_object(Hashie::Mash.new) do |(key, value), settings|
|
35
35
|
settings[key] = if value.respond_to? :each_pair
|
36
|
-
execute(value)
|
37
|
-
elsif key.match(
|
38
|
-
|
36
|
+
execute(value, namespace || key)
|
37
|
+
elsif key.match(secure_key_token)
|
38
|
+
encrypt(namespace, key, value)
|
39
39
|
else
|
40
40
|
value
|
41
41
|
end
|
42
42
|
end
|
43
|
+
end
|
43
44
|
|
44
|
-
|
45
|
+
def encryption_keys=(other)
|
46
|
+
@encryption_keys = other.each_with_object({}) do |(namespace, keyish), memo|
|
47
|
+
memo[namespace] = if keyish.is_a?(OpenSSL::PKey::RSA)
|
48
|
+
keyish
|
49
|
+
elsif ::File.readable?(::File.expand_path(keyish))
|
50
|
+
file_contents = ::File.read(::File.expand_path(keyish))
|
51
|
+
OpenSSL::PKey::RSA.new(file_contents)
|
52
|
+
else
|
53
|
+
OpenSSL::PKey::RSA.new(keyish)
|
54
|
+
end
|
55
|
+
end
|
45
56
|
end
|
46
57
|
|
47
|
-
|
48
|
-
return @encryption_key = nil if keyish.nil?
|
58
|
+
private
|
49
59
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
60
|
+
def encrypt(namespace, key, value)
|
61
|
+
method = encryption_method(value)
|
62
|
+
encryption_key = encryption_keys[namespace] || encryption_keys[:__default]
|
63
|
+
|
64
|
+
return value unless encryption_key
|
55
65
|
|
56
|
-
|
66
|
+
method.encrypt(key, value, encryption_key)
|
57
67
|
end
|
58
68
|
|
59
69
|
def encryption_method(value)
|
@@ -1,20 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'hashie/mash'
|
3
5
|
|
4
6
|
module Chamber
|
5
7
|
module Filters
|
6
8
|
class EnvironmentFilter
|
7
|
-
include Environmentable
|
8
|
-
|
9
|
-
def initialize(options = {})
|
10
|
-
self.data = options.fetch(:data)
|
11
|
-
end
|
12
|
-
|
13
9
|
###
|
14
10
|
# Internal: Allows the existing environment to be injected into the passed in
|
15
11
|
# hash. The hash that is passed in is *not* modified, instead a new hash is
|
16
12
|
# returned.
|
17
13
|
#
|
14
|
+
# This filter will also do basic value conversions from the environment
|
15
|
+
# variable string to the data type defined in the YAML. For example if the
|
16
|
+
# YAML value is `true`, then the conversion knows it's a Boolean. If there's
|
17
|
+
# an environment varible which should override that value, it will look to see
|
18
|
+
# if it is a `String` of 'true', 'false', 't', 'f', 'yes', or 'no' and perform
|
19
|
+
# the appropriate conversion of that value into a Boolean.
|
20
|
+
#
|
21
|
+
# This will work for:
|
22
|
+
#
|
23
|
+
# * Booleans
|
24
|
+
# * Integers
|
25
|
+
# * Floats
|
26
|
+
# * Arrays
|
27
|
+
#
|
28
|
+
# For the Arrays, it will convert the environment value by parsing the string
|
29
|
+
# as YAML. Whatever the parsed value ends up being, *must* be an Array.
|
30
|
+
#
|
18
31
|
# Examples:
|
19
32
|
#
|
20
33
|
# ###
|
@@ -37,6 +50,25 @@ class EnvironmentFilter
|
|
37
50
|
# }
|
38
51
|
#
|
39
52
|
# ###
|
53
|
+
# # Can do basic value conversions based on the raw data
|
54
|
+
# #
|
55
|
+
# ENV['LEVEL_ONE_1_LEVEL_TWO_1'] = '1'
|
56
|
+
# ENV['LEVEL_ONE_1_LEVEL_TWO_2_LEVEL_THREE_1'] = '[1, 2, 3]'
|
57
|
+
#
|
58
|
+
# EnvironmentFilter.execute(
|
59
|
+
# level_one_1: {
|
60
|
+
# level_two_1: 4,
|
61
|
+
# level_two_2: {
|
62
|
+
# level_three_1: [4, 5, 6] } } )
|
63
|
+
#
|
64
|
+
# # => {
|
65
|
+
# 'level_one_1' => {
|
66
|
+
# 'level_two_1' => 1,
|
67
|
+
# 'level_two_2' => {
|
68
|
+
# 'level_three_1' => [1, 2, 3],
|
69
|
+
# }
|
70
|
+
#
|
71
|
+
# ###
|
40
72
|
# # Can inject environment variables if said variables are prefixed
|
41
73
|
# #
|
42
74
|
# ENV['PREFIX_LEVEL_TWO_1'] = 'env value 1'
|
@@ -58,19 +90,80 @@ class EnvironmentFilter
|
|
58
90
|
new(options).__send__(:execute)
|
59
91
|
end
|
60
92
|
|
61
|
-
|
93
|
+
attr_accessor :data,
|
94
|
+
:secure_key_token
|
62
95
|
|
63
|
-
|
96
|
+
def initialize(options = {})
|
97
|
+
self.data = options.fetch(:data)
|
98
|
+
self.secure_key_token = /\A#{Regexp.escape(options.fetch(:secure_key_prefix))}/
|
99
|
+
end
|
100
|
+
|
101
|
+
protected
|
64
102
|
|
65
103
|
def execute(settings = data, parent_keys = [])
|
66
|
-
with_environment(
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
104
|
+
with_environment(
|
105
|
+
settings,
|
106
|
+
parent_keys,
|
107
|
+
lambda do |key, value, environment_keys|
|
108
|
+
{ key => execute(value, environment_keys) }
|
109
|
+
end,
|
110
|
+
lambda do |key, value, environment_key|
|
111
|
+
{ key => convert_environment_value(ENV[environment_key], value) }
|
112
|
+
end,
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def with_environment(settings, parent_keys, hash_block, value_block)
|
119
|
+
environment_hash = Hashie::Mash.new
|
120
|
+
|
121
|
+
settings.each_pair do |key, value|
|
122
|
+
environment_key = key.to_s.gsub(secure_key_token, '')
|
123
|
+
environment_keys = parent_keys.dup.push(environment_key)
|
124
|
+
|
125
|
+
if value.respond_to? :each_pair
|
126
|
+
environment_hash.merge!(hash_block.call(key, value, environment_keys))
|
127
|
+
else
|
128
|
+
environment_key = environment_keys.join('_').upcase
|
129
|
+
|
130
|
+
environment_hash.merge!(value_block.call(key, value, environment_key))
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
environment_hash
|
135
|
+
end
|
136
|
+
|
137
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
138
|
+
def convert_environment_value(environment_value, settings_value)
|
139
|
+
return settings_value unless environment_value
|
140
|
+
return if %w{___nil___ ___null___}.include?(environment_value)
|
141
|
+
|
142
|
+
case settings_value.class.name
|
143
|
+
when 'TrueClass', 'FalseClass'
|
144
|
+
case environment_value.downcase
|
145
|
+
when 'false', 'f', 'no', 'off', '0'
|
146
|
+
false
|
147
|
+
when 'true', 't', 'yes', 'on', '1'
|
148
|
+
true
|
149
|
+
else
|
150
|
+
fail ArgumentError, "Invalid value for Boolean: #{environment_value}"
|
151
|
+
end
|
152
|
+
when 'Float'
|
153
|
+
Float(environment_value)
|
154
|
+
when 'Array'
|
155
|
+
YAML.safe_load(environment_value).tap do |parsed_value|
|
156
|
+
unless parsed_value.is_a?(Array)
|
157
|
+
fail ArgumentError, "Invalid value for Array: #{environment_value}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
when 'Integer'
|
161
|
+
Integer(environment_value)
|
162
|
+
else
|
163
|
+
environment_value
|
164
|
+
end
|
73
165
|
end
|
166
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
74
167
|
end
|
75
168
|
end
|
76
169
|
end
|
@@ -1,23 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'chamber/errors/decryption_failure'
|
3
4
|
|
4
5
|
module Chamber
|
5
6
|
module Filters
|
6
7
|
class FailedDecryptionFilter
|
7
|
-
SECURE_KEY_TOKEN = /\A_secure_/
|
8
8
|
BASE64_STRING_PATTERN = %r{\A[A-Za-z0-9\+/]{342}==\z}
|
9
9
|
|
10
|
-
def initialize(options = {})
|
11
|
-
self.data = options.fetch(:data).dup
|
12
|
-
end
|
13
|
-
|
14
10
|
def self.execute(options = {})
|
15
11
|
new(options).__send__(:execute)
|
16
12
|
end
|
17
13
|
|
18
|
-
|
14
|
+
attr_accessor :data,
|
15
|
+
:secure_key_token
|
19
16
|
|
20
|
-
|
17
|
+
def initialize(options = {})
|
18
|
+
self.data = options.fetch(:data).dup
|
19
|
+
self.secure_key_token = /\A#{Regexp.escape(options.fetch(:secure_key_prefix))}/
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
21
23
|
|
22
24
|
def execute(raw_data = data)
|
23
25
|
settings = raw_data
|
@@ -25,7 +27,7 @@ class FailedDecryptionFilter
|
|
25
27
|
raw_data.each_pair do |key, value|
|
26
28
|
if value.respond_to? :each_pair
|
27
29
|
execute(value)
|
28
|
-
elsif key.match(
|
30
|
+
elsif key.match(secure_key_token) &&
|
29
31
|
value.respond_to?(:match) &&
|
30
32
|
value.match(BASE64_STRING_PATTERN)
|
31
33
|
|
@@ -1,23 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'hashie/mash'
|
3
4
|
|
4
5
|
module Chamber
|
5
6
|
module Filters
|
6
7
|
class NamespaceFilter
|
7
|
-
def initialize(options = {})
|
8
|
-
self.data = Hashie::Mash.new(options.fetch(:data))
|
9
|
-
self.namespaces = options.fetch(:namespaces)
|
10
|
-
end
|
11
|
-
|
12
8
|
def self.execute(options = {})
|
13
9
|
new(options).__send__(:execute)
|
14
10
|
end
|
15
11
|
|
16
|
-
protected
|
17
|
-
|
18
12
|
attr_accessor :data,
|
19
13
|
:namespaces
|
20
14
|
|
15
|
+
def initialize(options = {})
|
16
|
+
self.data = Hashie::Mash.new(options.fetch(:data))
|
17
|
+
self.namespaces = options.fetch(:namespaces)
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
21
22
|
def execute
|
22
23
|
if data_is_namespaced?
|
23
24
|
namespaces.each_with_object(Hashie::Mash.new) do |namespace, filtered_data|
|