vault-rails 0.3.1 → 0.7.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 +5 -5
- data/README.md +144 -11
- data/Rakefile +5 -2
- data/lib/vault/encrypted_model.rb +177 -59
- data/lib/vault/rails.rb +77 -29
- data/lib/vault/rails/configurable.rb +54 -8
- data/lib/vault/rails/errors.rb +8 -0
- data/lib/vault/rails/{serializer.rb → json_serializer.rb} +4 -5
- data/lib/vault/rails/version.rb +1 -1
- data/spec/dummy/app/models/lazy_person.rb +20 -0
- data/spec/dummy/app/models/lazy_single_person.rb +18 -0
- data/spec/dummy/app/models/person.rb +36 -1
- data/spec/dummy/config/environments/development.rb +5 -3
- data/spec/dummy/config/environments/test.rb +5 -3
- data/spec/dummy/db/migrate/20150428220101_create_people.rb +7 -1
- data/spec/dummy/db/schema.rb +21 -16
- data/spec/integration/rails_spec.rb +397 -17
- data/spec/lib/vault/rails/json_serializer_spec.rb +42 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/unit/encrypted_model_spec.rb +9 -4
- data/spec/unit/rails/configurable_spec.rb +118 -0
- data/spec/unit/vault/rails_spec.rb +33 -0
- metadata +55 -56
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +0 -220
- data/spec/dummy/log/test.log +0 -13
data/lib/vault/rails.rb
CHANGED
@@ -6,7 +6,7 @@ require "json"
|
|
6
6
|
require_relative "encrypted_model"
|
7
7
|
require_relative "rails/configurable"
|
8
8
|
require_relative "rails/errors"
|
9
|
-
require_relative "rails/
|
9
|
+
require_relative "rails/json_serializer"
|
10
10
|
require_relative "rails/version"
|
11
11
|
|
12
12
|
module Vault
|
@@ -26,6 +26,7 @@ module Vault
|
|
26
26
|
# The warning string to print when running in development mode.
|
27
27
|
DEV_WARNING = "[vault-rails] Using in-memory cipher - this is not secure " \
|
28
28
|
"and should never be used in production-like environments!".freeze
|
29
|
+
DEV_PREFIX = "vault:dev:".freeze
|
29
30
|
|
30
31
|
class << self
|
31
32
|
# API client object based off the configured options in {Configurable}.
|
@@ -72,7 +73,7 @@ module Vault
|
|
72
73
|
#
|
73
74
|
# @return [String]
|
74
75
|
# the encrypted cipher text
|
75
|
-
def encrypt(path, key, plaintext, client
|
76
|
+
def encrypt(path, key, plaintext, client: self.client, context: nil)
|
76
77
|
if plaintext.blank?
|
77
78
|
return plaintext
|
78
79
|
end
|
@@ -82,9 +83,9 @@ module Vault
|
|
82
83
|
|
83
84
|
with_retries do
|
84
85
|
if self.enabled?
|
85
|
-
result = self.vault_encrypt(path, key, plaintext, client)
|
86
|
+
result = self.vault_encrypt(path, key, plaintext, client: client, context: context)
|
86
87
|
else
|
87
|
-
result = self.memory_encrypt(path, key, plaintext, client)
|
88
|
+
result = self.memory_encrypt(path, key, plaintext, client: client, context: context)
|
88
89
|
end
|
89
90
|
|
90
91
|
return self.force_encoding(result)
|
@@ -104,7 +105,7 @@ module Vault
|
|
104
105
|
#
|
105
106
|
# @return [String]
|
106
107
|
# the decrypted plaintext text
|
107
|
-
def decrypt(path, key, ciphertext, client
|
108
|
+
def decrypt(path, key, ciphertext, client: self.client, context: nil)
|
108
109
|
if ciphertext.blank?
|
109
110
|
return ciphertext
|
110
111
|
end
|
@@ -114,9 +115,9 @@ module Vault
|
|
114
115
|
|
115
116
|
with_retries do
|
116
117
|
if self.enabled?
|
117
|
-
result = self.vault_decrypt(path, key, ciphertext, client)
|
118
|
+
result = self.vault_decrypt(path, key, ciphertext, client: client, context: context)
|
118
119
|
else
|
119
|
-
result = self.memory_decrypt(path, key, ciphertext, client)
|
120
|
+
result = self.memory_decrypt(path, key, ciphertext, client: client, context: context)
|
120
121
|
end
|
121
122
|
|
122
123
|
return self.force_encoding(result)
|
@@ -140,58 +141,101 @@ module Vault
|
|
140
141
|
end
|
141
142
|
end
|
142
143
|
|
144
|
+
def transform_encode(plaintext, opts={})
|
145
|
+
return plaintext if plaintext&.empty?
|
146
|
+
request_opts = {}
|
147
|
+
request_opts[:value] = plaintext
|
148
|
+
|
149
|
+
if opts[:transformation]
|
150
|
+
request_opts[:transformation] = opts[:transformation]
|
151
|
+
end
|
152
|
+
|
153
|
+
role_name = transform_role_name(opts)
|
154
|
+
client.transform.encode(role_name: role_name, **request_opts)
|
155
|
+
end
|
156
|
+
|
157
|
+
def transform_decode(ciphertext, opts={})
|
158
|
+
return ciphertext if ciphertext&.empty?
|
159
|
+
request_opts = {}
|
160
|
+
request_opts[:value] = ciphertext
|
161
|
+
|
162
|
+
if opts[:transformation]
|
163
|
+
request_opts[:transformation] = opts[:transformation]
|
164
|
+
end
|
165
|
+
|
166
|
+
role_name = transform_role_name(opts)
|
167
|
+
puts request_opts
|
168
|
+
client.transform.decode(role_name: role_name, **request_opts)
|
169
|
+
end
|
170
|
+
|
171
|
+
|
143
172
|
protected
|
144
173
|
|
145
174
|
# Perform in-memory encryption. This is useful for testing and development.
|
146
|
-
def memory_encrypt(path, key, plaintext, client)
|
147
|
-
log_warning(DEV_WARNING)
|
175
|
+
def memory_encrypt(path, key, plaintext, client: , context: nil)
|
176
|
+
log_warning(DEV_WARNING) if self.in_memory_warnings_enabled?
|
148
177
|
|
149
178
|
return nil if plaintext.nil?
|
150
179
|
|
151
180
|
cipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
152
181
|
cipher.encrypt
|
153
|
-
cipher.key = memory_key_for(path, key)
|
154
|
-
return Base64.strict_encode64(cipher.update(plaintext) + cipher.final)
|
182
|
+
cipher.key = memory_key_for(path, key, context: context)
|
183
|
+
return DEV_PREFIX + Base64.strict_encode64(cipher.update(plaintext) + cipher.final)
|
155
184
|
end
|
156
185
|
|
157
186
|
# Perform in-memory decryption. This is useful for testing and development.
|
158
|
-
def memory_decrypt(path, key, ciphertext, client)
|
159
|
-
log_warning(DEV_WARNING)
|
187
|
+
def memory_decrypt(path, key, ciphertext, client: , context: nil)
|
188
|
+
log_warning(DEV_WARNING) if self.in_memory_warnings_enabled?
|
160
189
|
|
161
190
|
return nil if ciphertext.nil?
|
162
191
|
|
192
|
+
raise Vault::Rails::InvalidCiphertext.new(ciphertext) if !ciphertext.start_with?(DEV_PREFIX)
|
193
|
+
data = ciphertext[DEV_PREFIX.length..-1]
|
194
|
+
|
163
195
|
cipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
164
196
|
cipher.decrypt
|
165
|
-
cipher.key = memory_key_for(path, key)
|
166
|
-
return cipher.update(Base64.strict_decode64(
|
197
|
+
cipher.key = memory_key_for(path, key, context: context)
|
198
|
+
return cipher.update(Base64.strict_decode64(data)) + cipher.final
|
199
|
+
end
|
200
|
+
|
201
|
+
# The symmetric key for the given params.
|
202
|
+
# @return [String]
|
203
|
+
def memory_key_for(path, key, context: nil)
|
204
|
+
md5 = OpenSSL::Digest::MD5.new
|
205
|
+
md5 << path
|
206
|
+
md5 << key
|
207
|
+
md5 << context if context
|
208
|
+
md5.digest
|
167
209
|
end
|
168
210
|
|
169
211
|
# Perform encryption using Vault. This will raise exceptions if Vault is
|
170
212
|
# unavailable.
|
171
|
-
def vault_encrypt(path, key, plaintext, client)
|
213
|
+
def vault_encrypt(path, key, plaintext, client: , context: nil)
|
172
214
|
return nil if plaintext.nil?
|
173
215
|
|
174
|
-
route
|
175
|
-
|
176
|
-
|
177
|
-
)
|
216
|
+
route = File.join(path, "encrypt", key)
|
217
|
+
|
218
|
+
data = { plaintext: Base64.strict_encode64(plaintext) }
|
219
|
+
data[:context] = Base64.strict_encode64(context) if context
|
220
|
+
|
221
|
+
secret = client.logical.write(route, data)
|
222
|
+
|
178
223
|
return secret.data[:ciphertext]
|
179
224
|
end
|
180
225
|
|
181
226
|
# Perform decryption using Vault. This will raise exceptions if Vault is
|
182
227
|
# unavailable.
|
183
|
-
def vault_decrypt(path, key, ciphertext, client)
|
228
|
+
def vault_decrypt(path, key, ciphertext, client: , context: nil)
|
184
229
|
return nil if ciphertext.nil?
|
185
230
|
|
186
|
-
route
|
187
|
-
secret = client.logical.write(route, ciphertext: ciphertext)
|
188
|
-
return Base64.strict_decode64(secret.data[:plaintext])
|
189
|
-
end
|
231
|
+
route = File.join(path, "decrypt", key)
|
190
232
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
233
|
+
data = { ciphertext: ciphertext }
|
234
|
+
data[:context] = Base64.strict_encode64(context) if context
|
235
|
+
|
236
|
+
secret = client.logical.write(route, data)
|
237
|
+
|
238
|
+
return Base64.strict_decode64(secret.data[:plaintext])
|
195
239
|
end
|
196
240
|
|
197
241
|
# Forces the encoding into the default Rails encoding and returns the
|
@@ -227,6 +271,10 @@ module Vault
|
|
227
271
|
::Rails.logger.warn { msg }
|
228
272
|
end
|
229
273
|
end
|
274
|
+
|
275
|
+
def transform_role_name(opts)
|
276
|
+
opts[:role] || self.default_role_name || self.application
|
277
|
+
end
|
230
278
|
end
|
231
279
|
end
|
232
280
|
end
|
@@ -10,10 +10,13 @@ module Vault
|
|
10
10
|
#
|
11
11
|
# @return [String]
|
12
12
|
def application
|
13
|
-
if
|
14
|
-
|
13
|
+
if defined?(@application) && !@application.nil?
|
14
|
+
return @application
|
15
15
|
end
|
16
|
-
|
16
|
+
if ENV.has_key?("VAULT_RAILS_APPLICATION")
|
17
|
+
return ENV["VAULT_RAILS_APPLICATION"]
|
18
|
+
end
|
19
|
+
raise RuntimeError, "Must set `Vault::Rails#application'!"
|
17
20
|
end
|
18
21
|
|
19
22
|
# Set the name of the application.
|
@@ -25,15 +28,18 @@ module Vault
|
|
25
28
|
|
26
29
|
# Whether the connection to Vault is enabled. The default value is `false`,
|
27
30
|
# which means vault-rails will perform in-memory encryption/decryption and
|
28
|
-
# not attempt to talk to a
|
31
|
+
# not attempt to talk to a real Vault server. This is useful for
|
29
32
|
# development and testing.
|
30
33
|
#
|
31
34
|
# @return [true, false]
|
32
35
|
def enabled?
|
33
|
-
if
|
34
|
-
return
|
36
|
+
if defined?(@enabled) && !@enabled.nil?
|
37
|
+
return @enabled
|
38
|
+
end
|
39
|
+
if ENV.has_key?("VAULT_RAILS_ENABLED")
|
40
|
+
return (ENV["VAULT_RAILS_ENABLED"] == "true")
|
35
41
|
end
|
36
|
-
return
|
42
|
+
return false
|
37
43
|
end
|
38
44
|
|
39
45
|
# Sets whether Vault is enabled. Users can set this in an initializer
|
@@ -49,6 +55,32 @@ module Vault
|
|
49
55
|
@enabled = !!val
|
50
56
|
end
|
51
57
|
|
58
|
+
# Whether warnings about in-memory ciphers are enabled. The default value
|
59
|
+
# is `true`, which means vault-rails will log a warning for every attempt
|
60
|
+
# to encrypt or decrypt using an in-memory cipher. This is useful for
|
61
|
+
# development and testing.
|
62
|
+
#
|
63
|
+
# @return [true, false]
|
64
|
+
def in_memory_warnings_enabled?
|
65
|
+
if !defined?(@in_memory_warnings_enabled) || @in_memory_warnings_enabled.nil?
|
66
|
+
return true
|
67
|
+
end
|
68
|
+
return @in_memory_warnings_enabled
|
69
|
+
end
|
70
|
+
|
71
|
+
# Sets whether warnings about in-memory ciphers are enabled. Users can set
|
72
|
+
# this in an initializer depending on their Rails environment.
|
73
|
+
#
|
74
|
+
# @example
|
75
|
+
# Vault.configure do |vault|
|
76
|
+
# vault.in_memory_warnings_enabled = !Rails.env.test?
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# @return [true, false]
|
80
|
+
def in_memory_warnings_enabled=(val)
|
81
|
+
@in_memory_warnings_enabled = val
|
82
|
+
end
|
83
|
+
|
52
84
|
# Gets the number of retry attempts.
|
53
85
|
#
|
54
86
|
# @return [Fixnum]
|
@@ -86,13 +118,27 @@ module Vault
|
|
86
118
|
@retry_max_wait ||= Vault::Defaults::RETRY_MAX_WAIT
|
87
119
|
end
|
88
120
|
|
89
|
-
# Sets the
|
121
|
+
# Sets the maximum amount of time for a single retry. Please see the Vault
|
90
122
|
# documentation for more information.
|
91
123
|
#
|
92
124
|
# @param [Fixnum] val
|
93
125
|
def retry_max_wait=(val)
|
94
126
|
@retry_max_wait = val
|
95
127
|
end
|
128
|
+
|
129
|
+
# Gets the default role name.
|
130
|
+
#
|
131
|
+
# @return [String]
|
132
|
+
def default_role_name
|
133
|
+
@default_role_name
|
134
|
+
end
|
135
|
+
|
136
|
+
# Sets the default role to use with various plugins.
|
137
|
+
#
|
138
|
+
# @param [String] val
|
139
|
+
def default_role_name=(val)
|
140
|
+
@default_role_name = val
|
141
|
+
end
|
96
142
|
end
|
97
143
|
end
|
98
144
|
end
|
data/lib/vault/rails/errors.rb
CHANGED
@@ -14,6 +14,14 @@ module Vault
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
class InvalidCiphertext < VaultRailsError
|
18
|
+
def initialize(ciphertext)
|
19
|
+
super <<~EOH
|
20
|
+
Invalid ciphertext: `#{ciphertext}'.
|
21
|
+
EOH
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
17
25
|
class ValidationFailedError < VaultRailsError; end
|
18
26
|
end
|
19
27
|
end
|
@@ -7,17 +7,16 @@ module Vault
|
|
7
7
|
}.freeze
|
8
8
|
|
9
9
|
def self.encode(raw)
|
10
|
-
|
11
|
-
|
12
|
-
raw = {} if raw.nil?
|
10
|
+
_init!
|
13
11
|
|
14
12
|
JSON.fast_generate(raw)
|
15
13
|
end
|
16
14
|
|
17
15
|
def self.decode(raw)
|
18
|
-
|
16
|
+
_init!
|
17
|
+
|
18
|
+
return nil if raw == nil || raw == ""
|
19
19
|
|
20
|
-
return {} if raw.nil? || raw.empty?
|
21
20
|
JSON.parse(raw, DECODE_OPTIONS)
|
22
21
|
end
|
23
22
|
|
data/lib/vault/rails/version.rb
CHANGED
@@ -25,4 +25,24 @@ class LazyPerson < ActiveRecord::Base
|
|
25
25
|
decode: ->(raw) { raw && raw[3...-3] }
|
26
26
|
|
27
27
|
vault_attribute :non_ascii
|
28
|
+
|
29
|
+
vault_attribute :default,
|
30
|
+
default: "abc123"
|
31
|
+
|
32
|
+
vault_attribute :default_with_serializer,
|
33
|
+
serialize: :json,
|
34
|
+
default: {}
|
35
|
+
|
36
|
+
vault_attribute :context_string,
|
37
|
+
context: "production"
|
38
|
+
|
39
|
+
vault_attribute :context_symbol,
|
40
|
+
context: :encryption_context
|
41
|
+
|
42
|
+
vault_attribute :context_proc,
|
43
|
+
context: ->(record) { record.encryption_context }
|
44
|
+
|
45
|
+
def encryption_context
|
46
|
+
"user_#{id}"
|
47
|
+
end
|
28
48
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
class LazySinglePerson < ActiveRecord::Base
|
3
|
+
include Vault::EncryptedModel
|
4
|
+
|
5
|
+
self.table_name = "people"
|
6
|
+
|
7
|
+
vault_lazy_decrypt!
|
8
|
+
vault_single_decrypt!
|
9
|
+
|
10
|
+
vault_attribute :ssn
|
11
|
+
|
12
|
+
vault_attribute :credit_card,
|
13
|
+
encrypted_column: :cc_encrypted
|
14
|
+
|
15
|
+
def encryption_context
|
16
|
+
"user_#{id}"
|
17
|
+
end
|
18
|
+
end
|
@@ -21,5 +21,40 @@ class Person < ActiveRecord::Base
|
|
21
21
|
decode: ->(raw) { raw && raw[3...-3] }
|
22
22
|
|
23
23
|
vault_attribute :non_ascii
|
24
|
-
end
|
25
24
|
|
25
|
+
vault_attribute :default,
|
26
|
+
default: "abc123"
|
27
|
+
|
28
|
+
vault_attribute :default_with_serializer,
|
29
|
+
serialize: :json,
|
30
|
+
default: {}
|
31
|
+
|
32
|
+
vault_attribute :context_string,
|
33
|
+
context: "production"
|
34
|
+
|
35
|
+
vault_attribute :context_symbol,
|
36
|
+
context: :encryption_context
|
37
|
+
|
38
|
+
vault_attribute :context_proc,
|
39
|
+
context: ->(record) { record.encryption_context }
|
40
|
+
|
41
|
+
vault_attribute :transform_ssn,
|
42
|
+
transform_secret: {
|
43
|
+
transformation: "social_sec"
|
44
|
+
}
|
45
|
+
|
46
|
+
vault_attribute :bad_transform,
|
47
|
+
transform_secret: {
|
48
|
+
transformation: "foobar_transformation"
|
49
|
+
}
|
50
|
+
|
51
|
+
vault_attribute :bad_role_transform,
|
52
|
+
transform_secret: {
|
53
|
+
transformation: "social_sec",
|
54
|
+
role: "foobar_role"
|
55
|
+
}
|
56
|
+
|
57
|
+
def encryption_context
|
58
|
+
"user_#{id}"
|
59
|
+
end
|
60
|
+
end
|
@@ -16,9 +16,6 @@ Rails.application.configure do
|
|
16
16
|
# Don't care if the mailer can't send.
|
17
17
|
config.action_mailer.raise_delivery_errors = false
|
18
18
|
|
19
|
-
# Print deprecation notices to the Rails logger.
|
20
|
-
config.active_support.deprecation = :log
|
21
|
-
|
22
19
|
# Raise an error on page load if there are pending migrations.
|
23
20
|
config.active_record.migration_error = :page_load
|
24
21
|
|
@@ -36,4 +33,9 @@ Rails.application.configure do
|
|
36
33
|
|
37
34
|
# Raises error for missing translations
|
38
35
|
# config.action_view.raise_on_missing_translations = true
|
36
|
+
|
37
|
+
# Use native SQLite integers as booleans in Rails 5.2+
|
38
|
+
if ActiveRecord.gem_version >= Gem::Version.new("5.2")
|
39
|
+
config.active_record.sqlite3.represent_boolean_as_integer = true
|
40
|
+
end
|
39
41
|
end
|
@@ -36,9 +36,11 @@ Rails.application.configure do
|
|
36
36
|
# ActionMailer::Base.deliveries array.
|
37
37
|
config.action_mailer.delivery_method = :test
|
38
38
|
|
39
|
-
# Print deprecation notices to the stderr.
|
40
|
-
config.active_support.deprecation = :stderr
|
41
|
-
|
42
39
|
# Raises error for missing translations
|
43
40
|
# config.action_view.raise_on_missing_translations = true
|
41
|
+
|
42
|
+
# Use native SQLite integers as booleans in Rails 5.2+
|
43
|
+
if ActiveRecord.gem_version >= Gem::Version.new("5.2")
|
44
|
+
config.active_record.sqlite3.represent_boolean_as_integer = true
|
45
|
+
end
|
44
46
|
end
|