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.
@@ -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
- class << self
8
- # The name of this application.
13
+ module Rails
14
+ extend Vault::Rails::Configurable
15
+
16
+ # The list of serializers.
9
17
  #
10
- # @return [String]
11
- attr_writer :application
18
+ # @return [Hash<Symbol, Module>]
19
+ SERIALIZERS = {
20
+ json: Vault::Rails::JSONSerializer,
21
+ }.freeze
12
22
 
13
- # The name of the application. This must be set or an error will be
14
- # returned.
23
+ # The default encoding.
15
24
  #
16
25
  # @return [String]
17
- def application
18
- if !defined?(@application) || @application.nil?
19
- raise RuntimeError, "Must set `Vault.application'!"
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
- return @application
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
- autoload :EncryptedModel, "vault/encrypted_model"
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
- route = File.join(path, "encrypt", key)
42
- secret = Vault.logical.write(route,
43
- plaintext: Base64.strict_encode64(plaintext),
44
- )
45
- return secret.data[:ciphertext]
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 = Vault.logical.write(route, ciphertext: ciphertext)
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
@@ -1,5 +1,5 @@
1
1
  module Vault
2
2
  module Rails
3
- VERSION = "0.1.2"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -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
@@ -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