vault-rails 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +93 -5
- data/lib/vault/encrypted_model.rb +55 -4
- data/lib/vault/rails.rb +37 -25
- data/lib/vault/rails/configurable.rb +1 -1
- 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/person.rb +20 -1
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20150428220101_create_people.rb +6 -1
- data/spec/dummy/db/schema.rb +15 -11
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +61050 -0
- data/spec/dummy/log/test.log +16195 -0
- data/spec/integration/rails_spec.rb +154 -7
- data/spec/lib/vault/rails/json_serializer_spec.rb +42 -0
- data/spec/unit/encrypted_model_spec.rb +6 -0
- data/spec/unit/vault/rails_spec.rb +33 -0
- metadata +43 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2aeef850725a949ef29fdb4ed9cc05ea02e8857a
|
4
|
+
data.tar.gz: 582d156fefa20941146a74eaa989c03e3dd2d948
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7ea41a6ca9a7c4e5c7d3298a4ef494b5a58a86b8005eb1c17f2f9b5d2e0da2c7e349fd0c13ae932f36992a7085c08d69b21b644123693b501237be0ae83e5338
|
7
|
+
data.tar.gz: 4c4b9a6ab95172c52338f6220a7ebe9c152dd22a283a0ed90d0ba35171fd08e8b4b08f57f732bcac4fc1e5e8ef8263b053b3e35363ad340f0f4d4425c02aea21
|
data/README.md
CHANGED
@@ -90,7 +90,7 @@ vault_attribute :credit_card,
|
|
90
90
|
- **Note** This value **cannot** be the same name as the vault attribute!
|
91
91
|
|
92
92
|
#### Specifying a custom key
|
93
|
-
By default, the name of the key in Vault is `#{app}_#{table}_#{column}`. This is customizable by setting the `:key`
|
93
|
+
By default, the name of the key in Vault is `#{app}_#{table}_#{column}`. This is customizable by setting the `:key` option when declaring the attribute:
|
94
94
|
|
95
95
|
```ruby
|
96
96
|
vault_attribute :credit_card,
|
@@ -99,7 +99,66 @@ vault_attribute :credit_card,
|
|
99
99
|
|
100
100
|
- **Note** Changing this value for an existing application will make existing values no longer decryptable!
|
101
101
|
|
102
|
+
#### Specifying a context (key derivation)
|
103
|
+
|
104
|
+
Vault Transit supports key derivation, which allows the same key to be used for multiple purposes by deriving a new key based on a context value.
|
105
|
+
|
106
|
+
The context can be specified as a string, symbol, or proc. Symbols (an instance method on the model) and procs are called for each encryption or decryption request, and should return a string.
|
107
|
+
|
108
|
+
- **Note** Changing the context or context generator for an attribute will make existing values no longer decryptable!
|
109
|
+
|
110
|
+
##### String
|
111
|
+
|
112
|
+
With a string, all records will use the same context for this attribute:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
vault_attribute :credit_card,
|
116
|
+
context: "user-cc"
|
117
|
+
```
|
118
|
+
|
119
|
+
##### Symbol
|
120
|
+
|
121
|
+
When using a symbol, a method will be called on the record to compute the context:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
belongs_to :user
|
125
|
+
|
126
|
+
vault_attribute :credit_card,
|
127
|
+
context: :encryption_context
|
128
|
+
|
129
|
+
def encryption_context
|
130
|
+
"user_#{user.id}"
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
##### Proc
|
135
|
+
|
136
|
+
Given a proc, it will be called each time to compute the context:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
belongs_to :user
|
140
|
+
|
141
|
+
vault_attribute :credit_card,
|
142
|
+
context: ->(record) { "user_#{record.user.id}" }
|
143
|
+
```
|
144
|
+
|
145
|
+
The proc must take a single argument for the record.
|
146
|
+
|
147
|
+
#### Specifying a default value
|
148
|
+
|
149
|
+
An attribute can specify a default value, which will be set on initialization (`.new`) or after loading the value from the database. The default will be set if the value is `nil`.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
vault_attribute :access_level,
|
153
|
+
default: "readonly"
|
154
|
+
|
155
|
+
vault_attribute :metadata,
|
156
|
+
serialize: :json,
|
157
|
+
default: {}
|
158
|
+
```
|
159
|
+
|
102
160
|
#### Specifying a different Vault path
|
161
|
+
|
103
162
|
By default, the path to the transit backend in Vault is `transit/`. This is customizable by setting the `:path` option when declaring the attribute:
|
104
163
|
|
105
164
|
```ruby
|
@@ -109,16 +168,45 @@ vault_attribute :credit_card,
|
|
109
168
|
|
110
169
|
- **Note** Changing this value for an existing application will make existing values no longer decryptable!
|
111
170
|
|
112
|
-
####
|
171
|
+
#### Lazy attribute decryption
|
172
|
+
By default, `vault-rails` will decrypt a record’s encrypted attributes on that record’s initializarion. You can configure an encrypted model to decrypt attributes lazily, which will prevent communication with Vault until an encrypted attribute’s getter method is called, at which point all of the record’s encrypted attributes will be decrypted. This is useful if you do not always need access to encrypted attributes. For example:
|
173
|
+
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class Person < ActiveRecord::Base
|
177
|
+
include Vault::EncryptedModel
|
178
|
+
vault_lazy_decrypt!
|
179
|
+
|
180
|
+
vault_attribute :ssn
|
181
|
+
end
|
182
|
+
|
183
|
+
# Without vault_lazy_decrypt:
|
184
|
+
person = Person.find(id) # Vault communication happens here
|
185
|
+
person.ssn
|
186
|
+
# => "123-45-6789"
|
187
|
+
|
188
|
+
# With vault_lazy_decrypt:
|
189
|
+
person = Person.find(id)
|
190
|
+
person.ssn # Vault communication happens here
|
191
|
+
# => "123-45-6789"
|
192
|
+
```
|
193
|
+
|
194
|
+
#### Serialization
|
195
|
+
|
113
196
|
By default, all values are assumed to be "text" fields in the database. Sometimes it is beneficial for your application to work with a more flexible data structure (such as a Hash or Array). Vault-rails can automatically serialize and deserialize these structures for you:
|
114
197
|
|
115
198
|
```ruby
|
116
|
-
vault_attribute :details
|
117
|
-
serialize: :json
|
199
|
+
vault_attribute :details,
|
200
|
+
serialize: :json,
|
201
|
+
default: {}
|
118
202
|
```
|
119
203
|
|
204
|
+
It is recommended to set a default matching type that you're serializing.
|
205
|
+
|
120
206
|
- **Note** You can view the source for the exact serialization and deserialization options, but they are intentionally not customizable and cannot be used for a full object marshal/unmarshal.
|
121
207
|
|
208
|
+
##### Custom Serializers
|
209
|
+
|
122
210
|
For customized solutions, you can also pass a module to the `:serializer` key. This module must have the following API:
|
123
211
|
|
124
212
|
```ruby
|
@@ -151,7 +239,7 @@ vault_attribute :address,
|
|
151
239
|
decode: ->(raw) { raw.to_s }
|
152
240
|
```
|
153
241
|
|
154
|
-
- **Note** Changing the algorithm for encoding/decoding for an existing application will probably make the application crash when attempting to
|
242
|
+
- **Note** Changing the algorithm for encoding/decoding for an existing application will probably make the application crash when attempting to retrieve existing values!
|
155
243
|
|
156
244
|
Caveats
|
157
245
|
-------
|
@@ -29,6 +29,12 @@ module Vault
|
|
29
29
|
# the path to the transit backend (default: +transit+)
|
30
30
|
# @option options [String] :key
|
31
31
|
# the name of the encryption key (default: +#{app}_#{table}_#{column}+)
|
32
|
+
# @option options [String, Symbol, Proc] :context
|
33
|
+
# either a string context, or a symbol or proc used to generate a
|
34
|
+
# context for key generation
|
35
|
+
# @option options [Object] :default
|
36
|
+
# a default value for this attribute to be set to if the underlying
|
37
|
+
# value is nil
|
32
38
|
# @option options [Symbol, Class] :serializer
|
33
39
|
# the name of the serializer to use (or a class)
|
34
40
|
# @option options [Proc] :encode
|
@@ -39,6 +45,8 @@ module Vault
|
|
39
45
|
encrypted_column = options[:encrypted_column] || "#{attribute}_encrypted"
|
40
46
|
path = options[:path] || "transit"
|
41
47
|
key = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{attribute}"
|
48
|
+
context = options[:context]
|
49
|
+
default = options[:default]
|
42
50
|
|
43
51
|
# Sanity check options!
|
44
52
|
_vault_validate_options!(options)
|
@@ -107,10 +115,12 @@ module Vault
|
|
107
115
|
|
108
116
|
# Make a note of this attribute so we can use it in the future (maybe).
|
109
117
|
__vault_attributes[attribute.to_sym] = {
|
118
|
+
context: context,
|
119
|
+
default: default,
|
120
|
+
encrypted_column: encrypted_column,
|
110
121
|
key: key,
|
111
122
|
path: path,
|
112
|
-
serializer: serializer
|
113
|
-
encrypted_column: encrypted_column,
|
123
|
+
serializer: serializer
|
114
124
|
}
|
115
125
|
|
116
126
|
self
|
@@ -142,6 +152,13 @@ module Vault
|
|
142
152
|
raise Vault::Rails::ValidationFailedError, "Cannot specify " \
|
143
153
|
"`:decode' without specifying `:encode' as well!"
|
144
154
|
end
|
155
|
+
|
156
|
+
if context = options[:context]
|
157
|
+
if context.is_a?(Proc) && context.arity != 1
|
158
|
+
raise Vault::Rails::ValidationFailedError, "Proc passed to " \
|
159
|
+
"`:context' must take 1 argument!"
|
160
|
+
end
|
161
|
+
end
|
145
162
|
end
|
146
163
|
|
147
164
|
def vault_lazy_decrypt
|
@@ -193,6 +210,8 @@ module Vault
|
|
193
210
|
path = options[:path]
|
194
211
|
serializer = options[:serializer]
|
195
212
|
column = options[:encrypted_column]
|
213
|
+
context = options[:context]
|
214
|
+
default = options[:default]
|
196
215
|
|
197
216
|
# Load the ciphertext
|
198
217
|
ciphertext = read_attribute(column)
|
@@ -203,14 +222,25 @@ module Vault
|
|
203
222
|
return
|
204
223
|
end
|
205
224
|
|
225
|
+
# Generate context if needed
|
226
|
+
generated_context = __vault_generate_context(context)
|
227
|
+
|
206
228
|
# Load the plaintext value
|
207
|
-
plaintext = Vault::Rails.decrypt(
|
229
|
+
plaintext = Vault::Rails.decrypt(
|
230
|
+
path, key, ciphertext,
|
231
|
+
context: generated_context
|
232
|
+
)
|
208
233
|
|
209
234
|
# Deserialize the plaintext value, if a serializer exists
|
210
235
|
if serializer
|
211
236
|
plaintext = serializer.decode(plaintext)
|
212
237
|
end
|
213
238
|
|
239
|
+
# Set to default if needed
|
240
|
+
if default && plaintext == nil
|
241
|
+
plaintext = default
|
242
|
+
end
|
243
|
+
|
214
244
|
# Write the virtual attribute with the plaintext value
|
215
245
|
instance_variable_set("@#{attribute}", plaintext)
|
216
246
|
end
|
@@ -244,6 +274,7 @@ module Vault
|
|
244
274
|
path = options[:path]
|
245
275
|
serializer = options[:serializer]
|
246
276
|
column = options[:encrypted_column]
|
277
|
+
context = options[:context]
|
247
278
|
|
248
279
|
# Only persist changed attributes to minimize requests - this helps
|
249
280
|
# minimize the number of requests to Vault.
|
@@ -259,8 +290,14 @@ module Vault
|
|
259
290
|
plaintext = serializer.encode(plaintext)
|
260
291
|
end
|
261
292
|
|
293
|
+
# Generate context if needed
|
294
|
+
generated_context = __vault_generate_context(context)
|
295
|
+
|
262
296
|
# Generate the ciphertext and store it back as an attribute
|
263
|
-
ciphertext = Vault::Rails.encrypt(
|
297
|
+
ciphertext = Vault::Rails.encrypt(
|
298
|
+
path, key, plaintext,
|
299
|
+
context: generated_context
|
300
|
+
)
|
264
301
|
|
265
302
|
# Write the attribute back, so that we don't have to reload the record
|
266
303
|
# to get the ciphertext
|
@@ -270,6 +307,20 @@ module Vault
|
|
270
307
|
{ column => ciphertext }
|
271
308
|
end
|
272
309
|
|
310
|
+
# Generates an Vault Transit encryption context for use on derived keys.
|
311
|
+
def __vault_generate_context(context)
|
312
|
+
case context
|
313
|
+
when String
|
314
|
+
context
|
315
|
+
when Symbol
|
316
|
+
send(context)
|
317
|
+
when Proc
|
318
|
+
context.call(self)
|
319
|
+
else
|
320
|
+
nil
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
273
324
|
# Override the reload method to reload the Vault attributes. This will
|
274
325
|
# ensure that we always have the most recent data from Vault when we
|
275
326
|
# reload a record from the database.
|
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
|
@@ -72,7 +72,7 @@ module Vault
|
|
72
72
|
#
|
73
73
|
# @return [String]
|
74
74
|
# the encrypted cipher text
|
75
|
-
def encrypt(path, key, plaintext, client
|
75
|
+
def encrypt(path, key, plaintext, client: self.client, context: nil)
|
76
76
|
if plaintext.blank?
|
77
77
|
return plaintext
|
78
78
|
end
|
@@ -82,9 +82,9 @@ module Vault
|
|
82
82
|
|
83
83
|
with_retries do
|
84
84
|
if self.enabled?
|
85
|
-
result = self.vault_encrypt(path, key, plaintext, client)
|
85
|
+
result = self.vault_encrypt(path, key, plaintext, client: client, context: context)
|
86
86
|
else
|
87
|
-
result = self.memory_encrypt(path, key, plaintext, client)
|
87
|
+
result = self.memory_encrypt(path, key, plaintext, client: client, context: context)
|
88
88
|
end
|
89
89
|
|
90
90
|
return self.force_encoding(result)
|
@@ -104,7 +104,7 @@ module Vault
|
|
104
104
|
#
|
105
105
|
# @return [String]
|
106
106
|
# the decrypted plaintext text
|
107
|
-
def decrypt(path, key, ciphertext, client
|
107
|
+
def decrypt(path, key, ciphertext, client: self.client, context: nil)
|
108
108
|
if ciphertext.blank?
|
109
109
|
return ciphertext
|
110
110
|
end
|
@@ -114,9 +114,9 @@ module Vault
|
|
114
114
|
|
115
115
|
with_retries do
|
116
116
|
if self.enabled?
|
117
|
-
result = self.vault_decrypt(path, key, ciphertext, client)
|
117
|
+
result = self.vault_decrypt(path, key, ciphertext, client: client, context: context)
|
118
118
|
else
|
119
|
-
result = self.memory_decrypt(path, key, ciphertext, client)
|
119
|
+
result = self.memory_decrypt(path, key, ciphertext, client: client, context: context)
|
120
120
|
end
|
121
121
|
|
122
122
|
return self.force_encoding(result)
|
@@ -143,55 +143,67 @@ module Vault
|
|
143
143
|
protected
|
144
144
|
|
145
145
|
# Perform in-memory encryption. This is useful for testing and development.
|
146
|
-
def memory_encrypt(path, key, plaintext, client)
|
146
|
+
def memory_encrypt(path, key, plaintext, client: , context: nil)
|
147
147
|
log_warning(DEV_WARNING) if self.in_memory_warnings_enabled?
|
148
148
|
|
149
149
|
return nil if plaintext.nil?
|
150
150
|
|
151
151
|
cipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
152
152
|
cipher.encrypt
|
153
|
-
cipher.key = memory_key_for(path, key)
|
153
|
+
cipher.key = memory_key_for(path, key, context: context)
|
154
154
|
return Base64.strict_encode64(cipher.update(plaintext) + cipher.final)
|
155
155
|
end
|
156
156
|
|
157
157
|
# Perform in-memory decryption. This is useful for testing and development.
|
158
|
-
def memory_decrypt(path, key, ciphertext, client)
|
158
|
+
def memory_decrypt(path, key, ciphertext, client: , context: nil)
|
159
159
|
log_warning(DEV_WARNING) if self.in_memory_warnings_enabled?
|
160
160
|
|
161
161
|
return nil if ciphertext.nil?
|
162
162
|
|
163
163
|
cipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
164
164
|
cipher.decrypt
|
165
|
-
cipher.key = memory_key_for(path, key)
|
165
|
+
cipher.key = memory_key_for(path, key, context: context)
|
166
166
|
return cipher.update(Base64.strict_decode64(ciphertext)) + cipher.final
|
167
167
|
end
|
168
168
|
|
169
|
+
# The symmetric key for the given params.
|
170
|
+
# @return [String]
|
171
|
+
def memory_key_for(path, key, context: nil)
|
172
|
+
md5 = OpenSSL::Digest::MD5.new
|
173
|
+
md5 << path
|
174
|
+
md5 << key
|
175
|
+
md5 << context if context
|
176
|
+
md5.digest
|
177
|
+
end
|
178
|
+
|
169
179
|
# Perform encryption using Vault. This will raise exceptions if Vault is
|
170
180
|
# unavailable.
|
171
|
-
def vault_encrypt(path, key, plaintext, client)
|
181
|
+
def vault_encrypt(path, key, plaintext, client: , context: nil)
|
172
182
|
return nil if plaintext.nil?
|
173
183
|
|
174
|
-
route
|
175
|
-
|
176
|
-
|
177
|
-
)
|
184
|
+
route = File.join(path, "encrypt", key)
|
185
|
+
|
186
|
+
data = { plaintext: Base64.strict_encode64(plaintext) }
|
187
|
+
data[:context] = Base64.strict_encode64(context) if context
|
188
|
+
|
189
|
+
secret = client.logical.write(route, data)
|
190
|
+
|
178
191
|
return secret.data[:ciphertext]
|
179
192
|
end
|
180
193
|
|
181
194
|
# Perform decryption using Vault. This will raise exceptions if Vault is
|
182
195
|
# unavailable.
|
183
|
-
def vault_decrypt(path, key, ciphertext, client)
|
196
|
+
def vault_decrypt(path, key, ciphertext, client: , context: nil)
|
184
197
|
return nil if ciphertext.nil?
|
185
198
|
|
186
|
-
route
|
187
|
-
secret = client.logical.write(route, ciphertext: ciphertext)
|
188
|
-
return Base64.strict_decode64(secret.data[:plaintext])
|
189
|
-
end
|
199
|
+
route = File.join(path, "decrypt", key)
|
190
200
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
201
|
+
data = { ciphertext: ciphertext }
|
202
|
+
data[:context] = Base64.strict_encode64(context) if context
|
203
|
+
|
204
|
+
secret = client.logical.write(route, data)
|
205
|
+
|
206
|
+
return Base64.strict_decode64(secret.data[:plaintext])
|
195
207
|
end
|
196
208
|
|
197
209
|
# Forces the encoding into the default Rails encoding and returns the
|
@@ -112,7 +112,7 @@ module Vault
|
|
112
112
|
@retry_max_wait ||= Vault::Defaults::RETRY_MAX_WAIT
|
113
113
|
end
|
114
114
|
|
115
|
-
# Sets the
|
115
|
+
# Sets the maximum amount of time for a single retry. Please see the Vault
|
116
116
|
# documentation for more information.
|
117
117
|
#
|
118
118
|
# @param [Fixnum] val
|
@@ -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
|