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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 264e4b8ae8a63d401b64370615831d735ee5341d
4
- data.tar.gz: f233fcdce5a44aa0eb2c25284b37e6ac23e10d96
3
+ metadata.gz: 2aeef850725a949ef29fdb4ed9cc05ea02e8857a
4
+ data.tar.gz: 582d156fefa20941146a74eaa989c03e3dd2d948
5
5
  SHA512:
6
- metadata.gz: 144e2eebb837082f62132bdd7ffe155658b567125a2daff5d11e130a8fd0fb1c0cfed75bdcb459a23916c6c1eb47a151199bc9f43389344f72b00c602e719cfe
7
- data.tar.gz: ead4d9b6091b9eafcfcd28a8e33a1df102b3230dbb2349c96309a5c7abc4f7eda8ff46485c04e080162e74acf19dc917e74366bbbc9328e729b22feb679cb4d0
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` coption when declaring the attribute:
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
- #### Automatic serializing
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 retrive existing values!
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(path, key, ciphertext)
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(path, key, plaintext)
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.
@@ -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/serializer"
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 = self.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 = self.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 = File.join(path, "encrypt", key)
175
- secret = client.logical.write(route,
176
- plaintext: Base64.strict_encode64(plaintext),
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 = File.join(path, "decrypt", key)
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
- # The symmetric key for the given params.
192
- # @return [String]
193
- def memory_key_for(path, key)
194
- return Base64.strict_encode64("#{path}/#{key}".ljust(16, "x")).byteslice(0..15)
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 naximum amount of time for a single retry. Please see the Vault
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
- self._init!
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
- self._init!
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
 
@@ -1,5 +1,5 @@
1
1
  module Vault
2
2
  module Rails
3
- VERSION = "0.4.0"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  end
@@ -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