vault-rails 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|