vault-rails 0.1.2 → 0.2.0
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 +4 -4
- data/README.md +116 -32
- data/lib/vault/encrypted_model.rb +215 -24
- data/lib/vault/rails.rb +183 -21
- data/lib/vault/rails/configurable.rb +98 -0
- data/lib/vault/rails/errors.rb +19 -0
- data/lib/vault/rails/serializer.rb +33 -0
- data/lib/vault/rails/version.rb +1 -1
- data/spec/dummy/app/models/person.rb +16 -0
- data/spec/dummy/config/initializers/vault.rb +2 -1
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20150428220101_create_people.rb +4 -0
- data/spec/dummy/db/schema.rb +6 -2
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/lib/binary_serializer.rb +12 -0
- data/spec/dummy/log/development.log +15591 -0
- data/spec/integration/rails_spec.rb +230 -6
- data/spec/support/vault_server.rb +14 -21
- data/spec/unit/encrypted_model_spec.rb +45 -0
- data/spec/unit/rails_spec.rb +14 -19
- metadata +29 -9
- data/lib/vault/rails/testing.rb +0 -73
data/lib/vault/rails.rb
CHANGED
@@ -3,29 +3,58 @@ require "vault"
|
|
3
3
|
require "base64"
|
4
4
|
require "json"
|
5
5
|
|
6
|
+
require_relative "encrypted_model"
|
7
|
+
require_relative "rails/configurable"
|
8
|
+
require_relative "rails/errors"
|
9
|
+
require_relative "rails/serializer"
|
10
|
+
require_relative "rails/version"
|
11
|
+
|
6
12
|
module Vault
|
7
|
-
|
8
|
-
|
13
|
+
module Rails
|
14
|
+
extend Vault::Rails::Configurable
|
15
|
+
|
16
|
+
# The list of serializers.
|
9
17
|
#
|
10
|
-
# @return [
|
11
|
-
|
18
|
+
# @return [Hash<Symbol, Module>]
|
19
|
+
SERIALIZERS = {
|
20
|
+
json: Vault::Rails::JSONSerializer,
|
21
|
+
}.freeze
|
12
22
|
|
13
|
-
# The
|
14
|
-
# returned.
|
23
|
+
# The default encoding.
|
15
24
|
#
|
16
25
|
# @return [String]
|
17
|
-
|
18
|
-
|
19
|
-
|
26
|
+
DEFAULT_ENCODING = "utf-8".freeze
|
27
|
+
|
28
|
+
# The warning string to print when running in development mode.
|
29
|
+
DEV_WARNING = "[vault-rails] Using in-memory cipher - this is not secure " \
|
30
|
+
"and should never be used in production-like environments!".freeze
|
31
|
+
|
32
|
+
# API client object based off the configured options in
|
33
|
+
# {Vault::Configurable}.
|
34
|
+
#
|
35
|
+
# @return [Vault::Client]
|
36
|
+
def self.client
|
37
|
+
if !defined?(@client) || !@client.same_options?(options)
|
38
|
+
@client = Vault::Client.new(options)
|
20
39
|
end
|
40
|
+
return @client
|
41
|
+
end
|
21
42
|
|
22
|
-
|
43
|
+
# Delegate all methods to the client object, essentially making the module
|
44
|
+
# object behave like a {Vault::Client}.
|
45
|
+
def self.method_missing(m, *args, &block)
|
46
|
+
if client.respond_to?(m)
|
47
|
+
client.public_send(m, *args, &block)
|
48
|
+
else
|
49
|
+
super
|
50
|
+
end
|
23
51
|
end
|
24
|
-
end
|
25
52
|
|
26
|
-
|
53
|
+
# Delegating `respond_to` to the {Vault::Client}.
|
54
|
+
def self.respond_to_missing?(m, include_private = false)
|
55
|
+
client.respond_to?(m) || super
|
56
|
+
end
|
27
57
|
|
28
|
-
module Rails
|
29
58
|
# Encrypt the given plaintext data using the provided mount and key.
|
30
59
|
#
|
31
60
|
# @param [String] path
|
@@ -34,15 +63,28 @@ module Vault
|
|
34
63
|
# the key to encrypt at
|
35
64
|
# @param [String] plaintext
|
36
65
|
# the plaintext to encrypt
|
66
|
+
# @param [Vault::Client] client
|
67
|
+
# the Vault client to use
|
37
68
|
#
|
38
69
|
# @return [String]
|
39
70
|
# the encrypted cipher text
|
40
|
-
def self.encrypt(path, key, plaintext)
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
71
|
+
def self.encrypt(path, key, plaintext, client = self.client)
|
72
|
+
if plaintext.blank?
|
73
|
+
return plaintext
|
74
|
+
end
|
75
|
+
|
76
|
+
path = path.to_s if !path.is_a?(String)
|
77
|
+
key = key.to_s if !key.is_a?(String)
|
78
|
+
|
79
|
+
with_retries do
|
80
|
+
if self.enabled?
|
81
|
+
result = self.vault_encrypt(path, key, plaintext, client)
|
82
|
+
else
|
83
|
+
result = self.memory_encrypt(path, key, plaintext, client)
|
84
|
+
end
|
85
|
+
|
86
|
+
return self.force_encoding(result)
|
87
|
+
end
|
46
88
|
end
|
47
89
|
|
48
90
|
# Decrypt the given ciphertext data using the provided mount and key.
|
@@ -53,13 +95,133 @@ module Vault
|
|
53
95
|
# the key to decrypt at
|
54
96
|
# @param [String] ciphertext
|
55
97
|
# the ciphertext to decrypt
|
98
|
+
# @param [Vault::Client] client
|
99
|
+
# the Vault client to use
|
56
100
|
#
|
57
101
|
# @return [String]
|
58
102
|
# the decrypted plaintext text
|
59
|
-
def self.decrypt(path, key, ciphertext)
|
103
|
+
def self.decrypt(path, key, ciphertext, client = self.client)
|
104
|
+
if ciphertext.blank?
|
105
|
+
return ciphertext
|
106
|
+
end
|
107
|
+
|
108
|
+
path = path.to_s if !path.is_a?(String)
|
109
|
+
key = key.to_s if !key.is_a?(String)
|
110
|
+
|
111
|
+
with_retries do
|
112
|
+
if self.enabled?
|
113
|
+
result = self.vault_decrypt(path, key, ciphertext, client)
|
114
|
+
else
|
115
|
+
result = self.memory_decrypt(path, key, ciphertext, client)
|
116
|
+
end
|
117
|
+
|
118
|
+
return self.force_encoding(result)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Get the serializer that corresponds to the given key. If the key does not
|
123
|
+
# correspond to a known serializer, an exception will be raised.
|
124
|
+
#
|
125
|
+
# @param [#to_sym] key
|
126
|
+
# the name of the serializer
|
127
|
+
#
|
128
|
+
# @return [~Serializer]
|
129
|
+
def self.serializer_for(key)
|
130
|
+
key = key.to_sym if !key.is_a?(Symbol)
|
131
|
+
|
132
|
+
if serializer = SERIALIZERS[key]
|
133
|
+
return serializer
|
134
|
+
else
|
135
|
+
raise Vault::Rails::UnknownSerializerError.new(key)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
# Perform in-memory encryption. This is useful for testing and development.
|
142
|
+
def self.memory_encrypt(path, key, plaintext, client)
|
143
|
+
log_warning(DEV_WARNING)
|
144
|
+
|
145
|
+
return nil if plaintext.nil?
|
146
|
+
|
147
|
+
cipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
148
|
+
cipher.encrypt
|
149
|
+
cipher.key = memory_key_for(path, key)
|
150
|
+
return Base64.strict_encode64(cipher.update(plaintext) + cipher.final)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Perform in-memory decryption. This is useful for testing and development.
|
154
|
+
def self.memory_decrypt(path, key, ciphertext, client)
|
155
|
+
log_warning(DEV_WARNING)
|
156
|
+
|
157
|
+
return nil if ciphertext.nil?
|
158
|
+
|
159
|
+
cipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
160
|
+
cipher.decrypt
|
161
|
+
cipher.key = memory_key_for(path, key)
|
162
|
+
return cipher.update(Base64.strict_decode64(ciphertext)) + cipher.final
|
163
|
+
end
|
164
|
+
|
165
|
+
# Perform encryption using Vault. This will raise exceptions if Vault is
|
166
|
+
# unavailable.
|
167
|
+
def self.vault_encrypt(path, key, plaintext, client)
|
168
|
+
return nil if plaintext.nil?
|
169
|
+
|
170
|
+
route = File.join(path, "encrypt", key)
|
171
|
+
secret = client.logical.write(route,
|
172
|
+
plaintext: Base64.strict_encode64(plaintext),
|
173
|
+
)
|
174
|
+
return secret.data[:ciphertext]
|
175
|
+
end
|
176
|
+
|
177
|
+
# Perform decryption using Vault. This will raise exceptions if Vault is
|
178
|
+
# unavailable.
|
179
|
+
def self.vault_decrypt(path, key, ciphertext, client)
|
180
|
+
return nil if ciphertext.nil?
|
181
|
+
|
60
182
|
route = File.join(path, "decrypt", key)
|
61
|
-
secret =
|
183
|
+
secret = client.logical.write(route, ciphertext: ciphertext)
|
62
184
|
return Base64.strict_decode64(secret.data[:plaintext])
|
63
185
|
end
|
186
|
+
|
187
|
+
# The symmetric key for the given params.
|
188
|
+
# @return [String]
|
189
|
+
def self.memory_key_for(path, key)
|
190
|
+
return Base64.strict_encode64("#{path}/#{key}".ljust(32, "x"))
|
191
|
+
end
|
192
|
+
|
193
|
+
# Forces the encoding into the default Rails encoding and returns the
|
194
|
+
# newly encoded string.
|
195
|
+
# @return [String]
|
196
|
+
def self.force_encoding(str)
|
197
|
+
encoding = ::Rails.application.config.encoding || DEFAULT_ENCODING
|
198
|
+
str.force_encoding(encoding).encode(encoding)
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
|
203
|
+
def self.with_retries(client = self.client, &block)
|
204
|
+
exceptions = [Vault::HTTPConnectionError, Vault::HTTPServerError]
|
205
|
+
options = {
|
206
|
+
attempts: self.retry_attempts,
|
207
|
+
base: self.retry_base,
|
208
|
+
max_wait: self.retry_max_wait,
|
209
|
+
}
|
210
|
+
|
211
|
+
client.with_retries(exceptions, options) do |i, e|
|
212
|
+
if !e.nil?
|
213
|
+
log_warning "[vault-rails] (#{i}) An error occurred when trying to " \
|
214
|
+
"communicate with Vault: #{e.message}"
|
215
|
+
end
|
216
|
+
|
217
|
+
yield
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.log_warning(msg)
|
222
|
+
if defined?(::Rails) && ::Rails.logger != nil
|
223
|
+
::Rails.logger.warn { msg }
|
224
|
+
end
|
225
|
+
end
|
64
226
|
end
|
65
227
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Vault
|
2
|
+
module Rails
|
3
|
+
module Configurable
|
4
|
+
include Vault::Configurable
|
5
|
+
|
6
|
+
# The name of the Vault::Rails application.
|
7
|
+
#
|
8
|
+
# @raise [RuntimeError]
|
9
|
+
# if the application has not been set
|
10
|
+
#
|
11
|
+
# @return [String]
|
12
|
+
def application
|
13
|
+
if !defined?(@application) || @application.nil?
|
14
|
+
raise RuntimeError, "Must set `Vault::Rails#application'!"
|
15
|
+
end
|
16
|
+
return @application
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set the name of the application.
|
20
|
+
#
|
21
|
+
# @param [String] val
|
22
|
+
def application=(val)
|
23
|
+
@application = val
|
24
|
+
end
|
25
|
+
|
26
|
+
# Whether the connection to Vault is enabled. The default value is `false`,
|
27
|
+
# 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
|
29
|
+
# development and testing.
|
30
|
+
#
|
31
|
+
# @return [true, false]
|
32
|
+
def enabled?
|
33
|
+
if !defined?(@enabled) || @enabled.nil?
|
34
|
+
return false
|
35
|
+
end
|
36
|
+
return @enabled
|
37
|
+
end
|
38
|
+
|
39
|
+
# Sets whether Vault is enabled. Users can set this in an initializer
|
40
|
+
# depending on their Rails environment.
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# Vault.configure do |vault|
|
44
|
+
# vault.enabled = Rails.env.production?
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# @return [true, false]
|
48
|
+
def enabled=(val)
|
49
|
+
@enabled = !!val
|
50
|
+
end
|
51
|
+
|
52
|
+
# Gets the number of retry attempts.
|
53
|
+
#
|
54
|
+
# @return [Fixnum]
|
55
|
+
def retry_attempts
|
56
|
+
@retry_attempts ||= 0
|
57
|
+
end
|
58
|
+
|
59
|
+
# Sets the number of retry attempts. Please see the Vault documentation
|
60
|
+
# for more information.
|
61
|
+
#
|
62
|
+
# @param [Fixnum] val
|
63
|
+
def retry_attempts=(val)
|
64
|
+
@retry_attempts = val
|
65
|
+
end
|
66
|
+
|
67
|
+
# Gets the number of retry attempts.
|
68
|
+
#
|
69
|
+
# @return [Fixnum]
|
70
|
+
def retry_base
|
71
|
+
@retry_base ||= Vault::Defaults::RETRY_BASE
|
72
|
+
end
|
73
|
+
|
74
|
+
# Sets the retry interval. Please see the Vault documentation for more
|
75
|
+
# information.
|
76
|
+
#
|
77
|
+
# @param [Fixnum] val
|
78
|
+
def retry_base=(val)
|
79
|
+
@retry_base = val
|
80
|
+
end
|
81
|
+
|
82
|
+
# Gets the retry maximum wait.
|
83
|
+
#
|
84
|
+
# @return [Fixnum]
|
85
|
+
def retry_max_wait
|
86
|
+
@retry_max_wait ||= Vault::Defaults::RETRY_MAX_WAIT
|
87
|
+
end
|
88
|
+
|
89
|
+
# Sets the naximum amount of time for a single retry. Please see the Vault
|
90
|
+
# documentation for more information.
|
91
|
+
#
|
92
|
+
# @param [Fixnum] val
|
93
|
+
def retry_max_wait=(val)
|
94
|
+
@retry_max_wait = val
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Vault
|
2
|
+
module Rails
|
3
|
+
class VaultRailsError < RuntimeError; end
|
4
|
+
|
5
|
+
class UnknownSerializerError < VaultRailsError
|
6
|
+
def initialize(key)
|
7
|
+
super <<-EOH
|
8
|
+
Unknown Vault serializer `:#{key}'. Valid serializers are:
|
9
|
+
|
10
|
+
#{SERIALIZERS.keys.sort.map(&:inspect).join(", ")}
|
11
|
+
|
12
|
+
Please refer to the documentation for more examples.
|
13
|
+
EOH
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class ValidationFailedError < VaultRailsError; end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Vault
|
2
|
+
module Rails
|
3
|
+
module JSONSerializer
|
4
|
+
DECODE_OPTIONS = {
|
5
|
+
max_nested: false,
|
6
|
+
create_additions: false,
|
7
|
+
}.freeze
|
8
|
+
|
9
|
+
def self.encode(raw)
|
10
|
+
self._init!
|
11
|
+
|
12
|
+
raw = {} if raw.nil?
|
13
|
+
|
14
|
+
JSON.fast_generate(raw)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.decode(raw)
|
18
|
+
self._init!
|
19
|
+
|
20
|
+
return {} if raw.nil? || raw.empty?
|
21
|
+
JSON.parse(raw, DECODE_OPTIONS)
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def self._init!
|
27
|
+
return if defined?(@_init)
|
28
|
+
require "json"
|
29
|
+
@_init = true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/vault/rails/version.rb
CHANGED
@@ -1,8 +1,24 @@
|
|
1
|
+
require "binary_serializer"
|
2
|
+
|
1
3
|
class Person < ActiveRecord::Base
|
2
4
|
include Vault::EncryptedModel
|
5
|
+
|
3
6
|
vault_attribute :ssn
|
7
|
+
|
4
8
|
vault_attribute :credit_card,
|
5
9
|
encrypted_column: :cc_encrypted,
|
6
10
|
path: "credit-secrets",
|
7
11
|
key: "people_credit_cards"
|
12
|
+
|
13
|
+
vault_attribute :details,
|
14
|
+
serialize: :json
|
15
|
+
|
16
|
+
vault_attribute :business_card,
|
17
|
+
serialize: BinarySerializer
|
18
|
+
|
19
|
+
vault_attribute :favorite_color,
|
20
|
+
encode: ->(raw) { "xxx#{raw}xxx" },
|
21
|
+
decode: ->(raw) { raw && raw[3...-3] }
|
22
|
+
|
23
|
+
vault_attribute :non_ascii
|
8
24
|
end
|
@@ -2,8 +2,9 @@ require "vault/rails"
|
|
2
2
|
|
3
3
|
require_relative "../../../support/vault_server"
|
4
4
|
|
5
|
-
Vault.configure do |vault|
|
5
|
+
Vault::Rails.configure do |vault|
|
6
6
|
vault.application = "dummy"
|
7
7
|
vault.address = RSpec::VaultServer.address
|
8
8
|
vault.token = RSpec::VaultServer.token
|
9
|
+
vault.enabled = true
|
9
10
|
end
|
Binary file
|
@@ -4,6 +4,10 @@ class CreatePeople < ActiveRecord::Migration
|
|
4
4
|
t.string :name
|
5
5
|
t.string :ssn_encrypted
|
6
6
|
t.string :cc_encrypted
|
7
|
+
t.string :details_encrypted
|
8
|
+
t.string :business_card_encrypted
|
9
|
+
t.string :favorite_color_encrypted
|
10
|
+
t.string :non_ascii_encrypted
|
7
11
|
|
8
12
|
t.timestamps null: false
|
9
13
|
end
|