vault-rails 0.3.2 → 0.7.1
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 +75 -27
- data/lib/vault/rails/configurable.rb +27 -7
- 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 +75 -0
- data/spec/unit/vault/rails_spec.rb +33 -0
- metadata +29 -24
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)
|
175
|
+
def memory_encrypt(path, key, plaintext, client: , context: nil)
|
147
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)
|
187
|
+
def memory_decrypt(path, key, ciphertext, client: , context: nil)
|
159
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.
|
@@ -30,10 +33,13 @@ module Vault
|
|
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
|
@@ -112,13 +118,27 @@ module Vault
|
|
112
118
|
@retry_max_wait ||= Vault::Defaults::RETRY_MAX_WAIT
|
113
119
|
end
|
114
120
|
|
115
|
-
# Sets the
|
121
|
+
# Sets the maximum amount of time for a single retry. Please see the Vault
|
116
122
|
# documentation for more information.
|
117
123
|
#
|
118
124
|
# @param [Fixnum] val
|
119
125
|
def retry_max_wait=(val)
|
120
126
|
@retry_max_wait = val
|
121
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
|
122
142
|
end
|
123
143
|
end
|
124
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
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class CreatePeople < ActiveRecord::Migration
|
1
|
+
class CreatePeople < ActiveRecord::Migration[4.2]
|
2
2
|
def change
|
3
3
|
create_table :people do |t|
|
4
4
|
t.string :name
|
@@ -8,6 +8,12 @@ class CreatePeople < ActiveRecord::Migration
|
|
8
8
|
t.string :business_card_encrypted
|
9
9
|
t.string :favorite_color_encrypted
|
10
10
|
t.string :non_ascii_encrypted
|
11
|
+
t.string :default_encrypted
|
12
|
+
t.string :default_with_serializer_encrypted
|
13
|
+
t.string :context_string_encrypted
|
14
|
+
t.string :context_symbol_encrypted
|
15
|
+
t.string :context_proc_encrypted
|
16
|
+
t.string :transform_ssn_encrypted
|
11
17
|
|
12
18
|
t.timestamps null: false
|
13
19
|
end
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -1,28 +1,33 @@
|
|
1
|
-
# encoding: UTF-8
|
2
1
|
# This file is auto-generated from the current state of the database. Instead
|
3
2
|
# of editing this file, please use the migrations feature of Active Record to
|
4
3
|
# incrementally modify your database, and then regenerate this schema definition.
|
5
4
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# from scratch.
|
10
|
-
#
|
5
|
+
# This file is the source Rails uses to define your schema when running `rails
|
6
|
+
# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
|
7
|
+
# be faster and is potentially less error prone than running all of your
|
8
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
9
|
+
# migrations use external dependencies or application code.
|
11
10
|
#
|
12
11
|
# It's strongly recommended that you check this file into your version control system.
|
13
12
|
|
14
|
-
ActiveRecord::Schema.define(version:
|
13
|
+
ActiveRecord::Schema.define(version: 2015_04_28_220101) do
|
15
14
|
|
16
15
|
create_table "people", force: :cascade do |t|
|
17
|
-
t.string
|
18
|
-
t.string
|
19
|
-
t.string
|
20
|
-
t.string
|
21
|
-
t.string
|
22
|
-
t.string
|
23
|
-
t.string
|
24
|
-
t.
|
25
|
-
t.
|
16
|
+
t.string "name"
|
17
|
+
t.string "ssn_encrypted"
|
18
|
+
t.string "cc_encrypted"
|
19
|
+
t.string "details_encrypted"
|
20
|
+
t.string "business_card_encrypted"
|
21
|
+
t.string "favorite_color_encrypted"
|
22
|
+
t.string "non_ascii_encrypted"
|
23
|
+
t.string "default_encrypted"
|
24
|
+
t.string "default_with_serializer_encrypted"
|
25
|
+
t.string "context_string_encrypted"
|
26
|
+
t.string "context_symbol_encrypted"
|
27
|
+
t.string "context_proc_encrypted"
|
28
|
+
t.string "transform_ssn_encrypted"
|
29
|
+
t.datetime "created_at", null: false
|
30
|
+
t.datetime "updated_at", null: false
|
26
31
|
end
|
27
32
|
|
28
33
|
end
|