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.
@@ -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