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.
@@ -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
@@ -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 = self.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 = self.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(ciphertext)) + cipher.final
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 = File.join(path, "encrypt", key)
175
- secret = client.logical.write(route,
176
- plaintext: Base64.strict_encode64(plaintext),
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 = File.join(path, "decrypt", key)
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
- # 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(32, "x"))
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 !defined?(@application) || @application.nil?
14
- raise RuntimeError, "Must set `Vault::Rails#application'!"
13
+ if defined?(@application) && !@application.nil?
14
+ return @application
15
15
  end
16
- return @application
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 reail Vault server. This is useful for
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 !defined?(@enabled) || @enabled.nil?
34
- return false
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 @enabled
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 naximum amount of time for a single retry. Please see the Vault
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
@@ -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
- 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.3.1"
3
+ VERSION = "0.7.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
@@ -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