vault-rails 0.3.1 → 0.7.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
- SHA1:
3
- metadata.gz: 47998d084dd52dda1d47a5f83744814782d82c0d
4
- data.tar.gz: 74419f45c8933e11f2ec4fef210c3b4fc876848c
2
+ SHA256:
3
+ metadata.gz: 60cc2b942cbd53163fc7effc3b0f47afc268f203ce079c444635ca9b4f14cc3a
4
+ data.tar.gz: d1c4e963e55205851a0e866eae50067f0a6c4839b53327ae6bb190d6ba98dd7f
5
5
  SHA512:
6
- metadata.gz: 27a4208b7a3c8a8b398a5b108b01c275bddfa73a8ec1747580227307ea5f0f87b0c2d184c73fd4740cd6be54f50f7be36afbda5590c5cd44d8d513a678d7741b
7
- data.tar.gz: 0cbfe20010c58fcb42a9c39d4a1e5e5bc5543a3d7b48a93e22d62fb8db726a5d06acb6d3be974c15909ce1766216e83d4b7c40af1c1fced8754a3c4471d73e37
6
+ metadata.gz: d08dbab43552537a3380e798efc70ba83694ac6e41bbd3e564c22ae0aead476c9080cf57685b9ac3a4e0719769ff91ce93a72c5bdd64b1a7593700a4306530a8
7
+ data.tar.gz: 594a5b3017457cee3369553b118ab67a105cabc42d7d6b52c4130953d954ec8063b4d52392088ce67f31a07ed6800134d1a5819c18378b312ede905ce9caea69
data/README.md CHANGED
@@ -1,16 +1,27 @@
1
- Vault Rails [![Build Status](https://secure.travis-ci.org/hashicorp/vault-rails.svg?branch=master)](http://travis-ci.org/hashicorp/vault-rails)
1
+ Vault Rails [![Build Status](https://circleci.com/gh/hashicorp/vault-rails.svg?style=shield)](https://circleci.com/gh/hashicorp/vault-rails)
2
2
  ===========
3
3
 
4
4
  Vault is the official Rails plugin for interacting with [Vault](https://vaultproject.io) by HashiCorp.
5
5
 
6
- **The documentation in this README corresponds to the master branch of the Vault Rails plugin. It may contain unreleased features or different APIs than the most recently released version. Please see the Git tag that corresponds to your version of the Vault Rails plugin for the proper documentation.**
6
+ **If you're viewing this README from GitHub on the `master` branch, know that it may contain unreleased features or
7
+ different APIs than the most recently released version. Please see the Git tag that corresponds to your version of the
8
+ Vault Rails plugin for the proper documentation.**
9
+
10
+ ## Table of Contents
11
+ 1. [Quick Start](#quick-start)
12
+ 1. [Advanced Configuration](#advanced-configuration)
13
+ 1. [Caveats](#caveats)
14
+ 1. [Development](#development)
15
+
7
16
 
8
17
  Quick Start
9
18
  -----------
19
+ ↥ [back to top](#table-of-contents)
20
+
10
21
  1. Add to your Gemfile:
11
22
 
12
23
  ```ruby
13
- gem "vault-rails", "~> 0.1", require: false
24
+ gem "vault-rails", require: false
14
25
  ```
15
26
 
16
27
  and then run the `bundle` command to install.
@@ -25,13 +36,13 @@ Quick Start
25
36
  # disabled, vault-rails will encrypt data in-memory using a similar
26
37
  # algorithm to Vault. The in-memory store uses a predictable encryption
27
38
  # which is great for development and test, but should _never_ be used in
28
- # production.
39
+ # production. Default: ENV["VAULT_RAILS_ENABLED"].
29
40
  vault.enabled = Rails.env.production?
30
41
 
31
42
  # The name of the application. All encrypted keys in Vault will be
32
43
  # prefixed with this application name. If you change the name of the
33
44
  # application, you will need to migrate the encrypted data to the new
34
- # key namespace.
45
+ # key namespace. Default: ENV["VAULT_RAILS_APPLICATION"].
35
46
  vault.application = "my_app"
36
47
 
37
48
  # The address of the Vault server. Default: ENV["VAULT_ADDR"].
@@ -60,7 +71,7 @@ Quick Start
60
71
 
61
72
  ```ruby
62
73
  class AddEncryptedSSNToPerson < ActiveRecord::Migration
63
- add_column :persons, :ssn_encrypted, :string
74
+ add_column :people, :ssn_encrypted, :string
64
75
  end
65
76
  ```
66
77
 
@@ -72,10 +83,13 @@ Quick Start
72
83
  person.save #=> true
73
84
  person.ssn_encrypted #=> "vault:v0:EE3EV8P5hyo9h..."
74
85
  ```
86
+ - **Note** The unencrypted value will still be saved if the attribute referenced has a corresponding column. (i.e. `ssn` in the case above)
75
87
 
76
88
 
77
89
  Advanced Configuration
78
90
  ----------------------
91
+ ↥ [back to top](#table-of-contents)
92
+
79
93
  The following section details some of the more advanced configuration options for vault-rails. As a general rule, you should try to use vault-rails without these options until absolutely necessary.
80
94
 
81
95
  #### Specifying the encrypted column
@@ -90,7 +104,7 @@ vault_attribute :credit_card,
90
104
  - **Note** This value **cannot** be the same name as the vault attribute!
91
105
 
92
106
  #### 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:
107
+ 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
108
 
95
109
  ```ruby
96
110
  vault_attribute :credit_card,
@@ -99,7 +113,66 @@ vault_attribute :credit_card,
99
113
 
100
114
  - **Note** Changing this value for an existing application will make existing values no longer decryptable!
101
115
 
116
+ #### Specifying a context (key derivation)
117
+
118
+ 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.
119
+
120
+ 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.
121
+
122
+ - **Note** Changing the context or context generator for an attribute will make existing values no longer decryptable!
123
+
124
+ ##### String
125
+
126
+ With a string, all records will use the same context for this attribute:
127
+
128
+ ```ruby
129
+ vault_attribute :credit_card,
130
+ context: "user-cc"
131
+ ```
132
+
133
+ ##### Symbol
134
+
135
+ When using a symbol, a method will be called on the record to compute the context:
136
+
137
+ ```ruby
138
+ belongs_to :user
139
+
140
+ vault_attribute :credit_card,
141
+ context: :encryption_context
142
+
143
+ def encryption_context
144
+ "user_#{user.id}"
145
+ end
146
+ ```
147
+
148
+ ##### Proc
149
+
150
+ Given a proc, it will be called each time to compute the context:
151
+
152
+ ```ruby
153
+ belongs_to :user
154
+
155
+ vault_attribute :credit_card,
156
+ context: ->(record) { "user_#{record.user.id}" }
157
+ ```
158
+
159
+ The proc must take a single argument for the record.
160
+
161
+ #### Specifying a default value
162
+
163
+ 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`.
164
+
165
+ ```ruby
166
+ vault_attribute :access_level,
167
+ default: "readonly"
168
+
169
+ vault_attribute :metadata,
170
+ serialize: :json,
171
+ default: {}
172
+ ```
173
+
102
174
  #### Specifying a different Vault path
175
+
103
176
  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
177
 
105
178
  ```ruby
@@ -109,16 +182,72 @@ vault_attribute :credit_card,
109
182
 
110
183
  - **Note** Changing this value for an existing application will make existing values no longer decryptable!
111
184
 
112
- #### Automatic serializing
185
+ #### Lazy attribute decryption
186
+ By default, `vault-rails` will decrypt a record’s encrypted attributes on that record’s initialization. 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:
187
+
188
+
189
+ ```ruby
190
+ class Person < ActiveRecord::Base
191
+ include Vault::EncryptedModel
192
+ vault_lazy_decrypt!
193
+
194
+ vault_attribute :ssn
195
+ end
196
+
197
+ # Without vault_lazy_decrypt:
198
+ person = Person.find(id) # Vault communication happens here
199
+ person.ssn
200
+ # => "123-45-6789"
201
+
202
+ # With vault_lazy_decrypt:
203
+ person = Person.find(id)
204
+ person.ssn # Vault communication happens here
205
+ # => "123-45-6789"
206
+ ```
207
+
208
+ #### Single, lazy attribute decryption
209
+ By default, `vault-rails` will decrypt all encrypted attributes on that record’s initialization on a class by class basis. You can configure an encrypted model to decrypt attributes lazily and and individually. This will prevent vault from loading all vault_attributes defined on a class the moment one attribute is requested.
210
+
211
+
212
+ ```ruby
213
+ class Person < ActiveRecord::Base
214
+ include Vault::EncryptedModel
215
+ vault_lazy_decrypt!
216
+ vault_single_decrypt!
217
+
218
+ vault_attribute :ssn
219
+ vault_attribute :email
220
+ end
221
+
222
+ # Without vault_single_decrypt:
223
+ person = Person.find(id) # Vault communication happens here
224
+ person.ssn # Vault communication happens here, fetches both ssn and email
225
+ # => "123-45-6789"
226
+
227
+ # With vault_single_decrypt:
228
+ person = Person.find(id)
229
+ person.ssn # Vault communication happens here, fetches only ssn
230
+ # => "123-45-6789"
231
+ person.email # Vault communication happens here, fetches only email
232
+ # => "foobar@baz.com"
233
+ ```
234
+
235
+ #### Serialization
236
+
113
237
  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
238
 
115
239
  ```ruby
116
- vault_attribute :details
117
- serialize: :json
240
+ vault_attribute :details,
241
+ serialize: :json,
242
+ default: {}
118
243
  ```
119
244
 
245
+ It is recommended to set a default matching type that you're serializing.
246
+
120
247
  - **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
248
 
249
+ ##### Custom Serializers
250
+
122
251
  For customized solutions, you can also pass a module to the `:serializer` key. This module must have the following API:
123
252
 
124
253
  ```ruby
@@ -151,10 +280,12 @@ vault_attribute :address,
151
280
  decode: ->(raw) { raw.to_s }
152
281
  ```
153
282
 
154
- - **Note** Changing the algorithm for encoding/decoding for an existing application will probably make the application crash when attempting to retrive existing values!
283
+ - **Note** Changing the algorithm for encoding/decoding for an existing application will probably make the application crash when attempting to retrieve existing values!
155
284
 
156
285
  Caveats
157
286
  -------
287
+ ↥ [back to top](#table-of-contents)
288
+
158
289
 
159
290
  ### Mounting/Creating Keys in Vault
160
291
  The Vault Rails plugin does not automatically mount a backend. It is assumed the proper backend is mounted and accessible by the given token. You can mount a transit backend like this:
@@ -217,6 +348,8 @@ the security model).
217
348
 
218
349
  Development
219
350
  -----------
351
+ ↥ [back to top](#table-of-contents)
352
+
220
353
  1. Clone the project on GitHub
221
354
  2. Create a feature branch
222
355
  3. Submit a Pull Request
data/Rakefile CHANGED
@@ -11,6 +11,9 @@ Bundler::GemHelper.install_tasks
11
11
  APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
12
12
  load "rails/tasks/engine.rake"
13
13
 
14
- require "rspec/core/rake_task"
15
- RSpec::Core::RakeTask.new(:spec)
16
14
  task default: :spec
15
+
16
+ require "rspec/core/rake_task"
17
+ RSpec::Core::RakeTask.new(:spec) do |t|
18
+ puts "\n==> Testing with Rails #{Rails::VERSION::STRING} and Ruby #{RUBY_VERSION} <==\n"
19
+ end
@@ -29,52 +29,55 @@ 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
35
41
  # a proc to encode the value with
36
42
  # @option options [Proc] :decode
37
43
  # a proc to decode the value with
44
+ # @option options [Hash] :transform_secret
45
+ # a hash providing details about the transformation to use,
46
+ # this includes the name, and the role to use
38
47
  def vault_attribute(attribute, options = {})
39
- encrypted_column = options[:encrypted_column] || "#{attribute}_encrypted"
40
- path = options[:path] || "transit"
41
- key = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{attribute}"
42
-
43
48
  # Sanity check options!
44
49
  _vault_validate_options!(options)
45
50
 
46
- # Get the serializer if one was given.
47
- serializer = options[:serialize]
51
+ parsed_opts = if options[:transform_secret]
52
+ parse_transform_secret_attributes(attribute, options)
53
+ else
54
+ parse_transit_attributes(attribute, options)
55
+ end
56
+ parsed_opts[:encrypted_column] = options[:encrypted_column] || "#{attribute}_encrypted"
48
57
 
49
- # Unless a class or module was given, construct our serializer. (Slass
50
- # is a subset of Module).
51
- if serializer && !serializer.is_a?(Module)
52
- serializer = Vault::Rails.serializer_for(serializer)
53
- end
58
+ # Make a note of this attribute so we can use it in the future (maybe).
59
+ __vault_attributes[attribute.to_sym] = parsed_opts
54
60
 
55
- # See if custom encoding or decoding options were given.
56
- if options[:encode] && options[:decode]
57
- serializer = Class.new
58
- serializer.define_singleton_method(:encode, &options[:encode])
59
- serializer.define_singleton_method(:decode, &options[:decode])
60
- end
61
+ self.attribute attribute.to_s, ActiveRecord::Type::Value.new,
62
+ default: nil
61
63
 
62
64
  # Getter
63
65
  define_method("#{attribute}") do
64
- self.__vault_load_attributes! unless @__vault_loaded
65
- instance_variable_get("@#{attribute}")
66
+ self.__vault_load_attributes!(attribute) unless @__vault_loaded
67
+ super()
66
68
  end
67
69
 
68
70
  # Setter
69
71
  define_method("#{attribute}=") do |value|
70
- self.__vault_load_attributes! unless @__vault_loaded
72
+ self.__vault_load_attributes!(attribute) unless @__vault_loaded
71
73
 
72
74
  # We always set it as changed without comparing with the current value
73
75
  # because we allow our held values to be mutated, so we need to assume
74
- # that if you call attr=, you want it send back regardless.
76
+ # that if you call attr=, you want it sent back regardless.
75
77
 
76
78
  attribute_will_change!("#{attribute}")
77
79
  instance_variable_set("@#{attribute}", value)
80
+ super(value)
78
81
 
79
82
  # Return the value to be consistent with other AR methods.
80
83
  value
@@ -82,37 +85,10 @@ module Vault
82
85
 
83
86
  # Checker
84
87
  define_method("#{attribute}?") do
85
- self.__vault_load_attributes! unless @__vault_loaded
88
+ self.__vault_load_attributes!(attribute) unless @__vault_loaded
86
89
  instance_variable_get("@#{attribute}").present?
87
90
  end
88
91
 
89
- # Dirty method
90
- define_method("#{attribute}_change") do
91
- changes["#{attribute}"]
92
- end
93
-
94
- # Dirty method
95
- define_method("#{attribute}_changed?") do
96
- changed.include?("#{attribute}")
97
- end
98
-
99
- # Dirty method
100
- define_method("#{attribute}_was") do
101
- if changes["#{attribute}"]
102
- changes["#{attribute}"][0]
103
- else
104
- public_send("#{attribute}")
105
- end
106
- end
107
-
108
- # Make a note of this attribute so we can use it in the future (maybe).
109
- __vault_attributes[attribute.to_sym] = {
110
- key: key,
111
- path: path,
112
- serializer: serializer,
113
- encrypted_column: encrypted_column,
114
- }
115
-
116
92
  self
117
93
  end
118
94
 
@@ -131,6 +107,11 @@ module Vault
131
107
  raise Vault::Rails::ValidationFailedError, "Cannot use a " \
132
108
  "custom encoder/decoder if a `:serializer' is specified!"
133
109
  end
110
+
111
+ if options[:transform_secret]
112
+ raise Vault::Rails::ValidationFailedError, "Cannot use the " \
113
+ "transform secrets engine with a specified `:serializer'!"
114
+ end
134
115
  end
135
116
 
136
117
  if options[:encode] && !options[:decode]
@@ -142,6 +123,19 @@ module Vault
142
123
  raise Vault::Rails::ValidationFailedError, "Cannot specify " \
143
124
  "`:decode' without specifying `:encode' as well!"
144
125
  end
126
+
127
+ if context = options[:context]
128
+ if context.is_a?(Proc) && context.arity != 1
129
+ raise Vault::Rails::ValidationFailedError, "Proc passed to " \
130
+ "`:context' must take 1 argument!"
131
+ end
132
+ end
133
+ if transform_opts = options[:transform_secret]
134
+ if !transform_opts[:transformation]
135
+ raise Vault::Rails::VaildationFailedError, "Transform Secrets " \
136
+ "requires a transformation name!"
137
+ end
138
+ end
145
139
  end
146
140
 
147
141
  def vault_lazy_decrypt
@@ -151,6 +145,62 @@ module Vault
151
145
  def vault_lazy_decrypt!
152
146
  @vault_lazy_decrypt = true
153
147
  end
148
+
149
+ def vault_single_decrypt
150
+ @vault_single_decrypt ||= false
151
+ end
152
+
153
+ def vault_single_decrypt!
154
+ @vault_single_decrypt = true
155
+ end
156
+
157
+ private
158
+
159
+ def parse_transform_secret_attributes(attribute, options)
160
+ opts = {}
161
+ opts[:transform_secret] = true
162
+
163
+ serializer = Class.new
164
+ serializer.define_singleton_method(:encode) do |raw|
165
+ return if raw.nil?
166
+ resp = Vault::Rails.transform_encode(raw, options[:transform_secret])
167
+ resp.dig(:data, :encoded_value)
168
+ end
169
+ serializer.define_singleton_method(:decode) do |raw|
170
+ return if raw.nil?
171
+ resp = Vault::Rails.transform_decode(raw, options[:transform_secret])
172
+ resp.dig(:data, :decoded_value)
173
+ end
174
+ opts[:serializer] = serializer
175
+ opts
176
+ end
177
+
178
+ def parse_transit_attributes(attribute, options)
179
+ opts = {}
180
+ opts[:path] = options[:path] || "transit"
181
+ opts[:key] = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{attribute}"
182
+ opts[:context] = options[:context]
183
+ opts[:default] = options[:default]
184
+
185
+ # Get the serializer if one was given.
186
+ serializer = options[:serialize]
187
+
188
+ # Unless a class or module was given, construct our serializer. (Slass
189
+ # is a subset of Module).
190
+ if serializer && !serializer.is_a?(Module)
191
+ serializer = Vault::Rails.serializer_for(serializer)
192
+ end
193
+
194
+ # See if custom encoding or decoding options were given.
195
+ if options[:encode] && options[:decode]
196
+ serializer = Class.new
197
+ serializer.define_singleton_method(:encode, &options[:encode])
198
+ serializer.define_singleton_method(:decode, &options[:decode])
199
+ end
200
+
201
+ opts[:serializer] = serializer
202
+ opts
203
+ end
154
204
  end
155
205
 
156
206
  included do
@@ -166,6 +216,12 @@ module Vault
166
216
  # before theirs, resulting in attributes that are not persisted.
167
217
  after_save :__vault_persist_attributes!
168
218
 
219
+ # Before destroying a record, ensure that all vault attributes have been
220
+ # decrypted. Otherwise, attempting to read a lazily decrypted attribute
221
+ # after a destroy will result in a "Can't modify frozen Hash" error when
222
+ # vault-rails attempts to set the plain-text attribute.
223
+ before_destroy :__vault_load_attributes!, unless: -> { @__vault_loaded }
224
+
169
225
  # Decrypt all the attributes from Vault.
170
226
  # @return [true]
171
227
  def __vault_initialize_attributes!
@@ -177,12 +233,16 @@ module Vault
177
233
  __vault_load_attributes!
178
234
  end
179
235
 
180
- def __vault_load_attributes!
236
+ def __vault_load_attributes!(attribute_to_read = nil)
181
237
  self.class.__vault_attributes.each do |attribute, options|
238
+ # skip loading certain keys in one of two cases:
239
+ # 1- the attribute has already been loaded
240
+ # 2- the single decrypt option is set AND this is not the attribute we're requesting to decrypt
241
+ next if instance_variable_get("@#{attribute}") || (self.class.vault_single_decrypt && attribute_to_read != attribute)
182
242
  self.__vault_load_attribute!(attribute, options)
183
243
  end
184
244
 
185
- @__vault_loaded = true
245
+ @__vault_loaded = self.class.__vault_attributes.all? { |attribute, __| instance_variable_defined?("@#{attribute}") }
186
246
 
187
247
  return true
188
248
  end
@@ -193,26 +253,48 @@ module Vault
193
253
  path = options[:path]
194
254
  serializer = options[:serializer]
195
255
  column = options[:encrypted_column]
256
+ context = options[:context]
257
+ default = options[:default]
258
+ transform = options[:transform_secret]
196
259
 
197
260
  # Load the ciphertext
198
261
  ciphertext = read_attribute(column)
199
262
 
200
263
  # If the user provided a value for the attribute, do not try to load
201
264
  # it from Vault
202
- if instance_variable_get("@#{attribute}")
265
+ if attributes[attribute.to_s]
203
266
  return
204
267
  end
205
268
 
206
- # Load the plaintext value
207
- plaintext = Vault::Rails.decrypt(path, key, ciphertext)
269
+ # Generate context if needed
270
+ generated_context = __vault_generate_context(context)
271
+
272
+ if transform
273
+ # If this is a secret encrypted with FPE, we do not need to decrypt with vault
274
+ # This prevents a double encryption via standard vault encryption and FPE.
275
+ # FPE is decrypted later as part of the serializer
276
+ plaintext = ciphertext
277
+ else
278
+ # Load the plaintext value
279
+ plaintext = Vault::Rails.decrypt(
280
+ path, key, ciphertext,
281
+ context: generated_context
282
+ )
283
+ end
208
284
 
209
285
  # Deserialize the plaintext value, if a serializer exists
210
286
  if serializer
211
287
  plaintext = serializer.decode(plaintext)
212
288
  end
213
289
 
290
+ # Set to default if needed
291
+ if default && plaintext == nil
292
+ plaintext = default
293
+ end
294
+
214
295
  # Write the virtual attribute with the plaintext value
215
296
  instance_variable_set("@#{attribute}", plaintext)
297
+ @attributes.write_from_database attribute.to_s, plaintext
216
298
  end
217
299
 
218
300
  # Encrypt all the attributes using Vault and set the encrypted values back
@@ -244,23 +326,44 @@ module Vault
244
326
  path = options[:path]
245
327
  serializer = options[:serializer]
246
328
  column = options[:encrypted_column]
329
+ context = options[:context]
330
+ transform = options[:transform_secret]
247
331
 
248
332
  # Only persist changed attributes to minimize requests - this helps
249
333
  # minimize the number of requests to Vault.
250
- if !changed.include?("#{attribute}")
251
- return
334
+ if ActiveRecord.gem_version >= Gem::Version.new("6.0")
335
+ return unless previous_changes.include?(attribute)
336
+ elsif ActiveRecord.gem_version >= Gem::Version.new("5.2")
337
+ return unless previous_changes_include?(attribute)
338
+ elsif ActiveRecord.gem_version >= Gem::Version.new("5.1")
339
+ return unless saved_change_to_attribute?(attribute.to_s)
340
+ else
341
+ return unless attribute_changed?(attribute)
252
342
  end
253
343
 
254
344
  # Get the current value of the plaintext attribute
255
- plaintext = instance_variable_get("@#{attribute}")
345
+ plaintext = attributes[attribute.to_s]
256
346
 
257
347
  # Apply the serialize to the plaintext value, if one exists
258
348
  if serializer
259
349
  plaintext = serializer.encode(plaintext)
260
350
  end
261
351
 
262
- # Generate the ciphertext and store it back as an attribute
263
- ciphertext = Vault::Rails.encrypt(path, key, plaintext)
352
+ # Generate context if needed
353
+ generated_context = __vault_generate_context(context)
354
+
355
+ if transform
356
+ # If this is a secret encrypted with FPE, we should not encrypt it in vault
357
+ # This prevents a double encryption via standard vault encryption and FPE.
358
+ # FPE was performed earlier as part of the serialization process.
359
+ ciphertext = plaintext
360
+ else
361
+ # Generate the ciphertext and store it back as an attribute
362
+ ciphertext = Vault::Rails.encrypt(
363
+ path, key, plaintext,
364
+ context: generated_context
365
+ )
366
+ end
264
367
 
265
368
  # Write the attribute back, so that we don't have to reload the record
266
369
  # to get the ciphertext
@@ -270,6 +373,20 @@ module Vault
270
373
  { column => ciphertext }
271
374
  end
272
375
 
376
+ # Generates an Vault Transit encryption context for use on derived keys.
377
+ def __vault_generate_context(context)
378
+ case context
379
+ when String
380
+ context
381
+ when Symbol
382
+ send(context)
383
+ when Proc
384
+ context.call(self)
385
+ else
386
+ nil
387
+ end
388
+ end
389
+
273
390
  # Override the reload method to reload the Vault attributes. This will
274
391
  # ensure that we always have the most recent data from Vault when we
275
392
  # reload a record from the database.
@@ -279,6 +396,7 @@ module Vault
279
396
  # from Vault
280
397
  self.class.__vault_attributes.each do |attribute, _|
281
398
  self.instance_variable_set("@#{attribute}", nil)
399
+ @attributes.write_from_database attribute.to_s, nil
282
400
  end
283
401
 
284
402
  self.__vault_initialize_attributes!