lockbox 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10f6fa1f09a73c4fb740dce62478c0abc65dbfa6ed5434801429bf5d990e2e38
4
- data.tar.gz: b6975e18f7f9c28ce7397f982a6f3900fc6af7938883e0631afbd72e32d5caec
3
+ metadata.gz: d4bf60bca21cbcbf37397b2431401a56191b2a8a2b311ddd12bbe8f94d43a259
4
+ data.tar.gz: '09969a1e9c7904fe69e017dd6aa8a0d5e1a50573fc5937994ad2c0eab63f499b'
5
5
  SHA512:
6
- metadata.gz: 322b03e672c6e389f26311e57625b6f6e633c64dbd27904a6c5c59105b809b295edd2d49171965ad6c6f6707e8a7d4ddf191b2612b7563326a9c867cabcc9b57
7
- data.tar.gz: 2a4cec96d5b0388bae885cb817aba5ada8ac1c65d74f3fc310800a207f500eba15d5de475c7b94e81027f7b4f09599c9a4b5c65145b6996e6ca9eda7215704a4
6
+ metadata.gz: 5b513fb92e0074f10a5044acbcd9a06bba8f90af473cdc52ae7d981e5432e77afedca887372246afbbb16df7c72cc099ad12a59312e909ef36203c9678b2d987
7
+ data.tar.gz: 1ab527b1cad1a112b1ce43a3e2745faff13289dfa9dabcd15d7e0ca856cfec801b41112faa09ca312dd57537f3ac5a32d5882fb369d2bb0e262f8bd2bd20d8b4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.2.4
2
+
3
+ - Added support for Mongoid
4
+ - Added `encrypt_io` and `decrypt_io` methods
5
+ - Made it easier to rotate algorithms with master key
6
+ - Fixed error with migrate and default scope
7
+ - Fixed encryption with Active Storage 6 and `record.create!`
8
+
1
9
  ## 0.2.3
2
10
 
3
11
  - Added time type
data/README.md CHANGED
@@ -46,6 +46,8 @@ Alternatively, you can use a [key management service](#key-management) to manage
46
46
 
47
47
  ## Database Fields
48
48
 
49
+ ### Active Record
50
+
49
51
  Create a migration with:
50
52
 
51
53
  ```ruby
@@ -72,7 +74,7 @@ User.create!(email: "hi@example.org")
72
74
 
73
75
  If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index).
74
76
 
75
- ### Types
77
+ #### Types
76
78
 
77
79
  Specify the type of a field with:
78
80
 
@@ -101,10 +103,30 @@ class User < ApplicationRecord
101
103
  end
102
104
  ```
103
105
 
104
- ### Validations
106
+ #### Validations
105
107
 
106
108
  Validations work as expected with the exception of uniqueness. Uniqueness validations require a [blind index](https://github.com/ankane/blind_index).
107
109
 
110
+ ### Mongoid
111
+
112
+ Add to your model:
113
+
114
+ ```ruby
115
+ class User
116
+ field :email_ciphertext, type: String
117
+
118
+ encrypts :email
119
+ end
120
+ ```
121
+
122
+ You can use `email` just like any other attribute.
123
+
124
+ ```ruby
125
+ User.create!(email: "hi@example.org")
126
+ ```
127
+
128
+ If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index).
129
+
108
130
  ## Files
109
131
 
110
132
  ### Active Storage
@@ -140,37 +162,58 @@ def license
140
162
  end
141
163
  ```
142
164
 
143
- **Note:** With Rails 6, attachments are not encrypted with:
165
+ ### CarrierWave
166
+
167
+ Add to your uploader:
168
+
169
+ ```ruby
170
+ class LicenseUploader < CarrierWave::Uploader::Base
171
+ encrypt
172
+ end
173
+ ```
174
+
175
+ Encryption is applied to all versions after processing.
176
+
177
+ To serve encrypted files, use a controller action.
144
178
 
145
179
  ```ruby
146
- User.create!(avatar: params[:avatar])
180
+ def license
181
+ send_data @user.license.read, type: @user.license.content_type
182
+ end
147
183
  ```
148
184
 
149
- Until this is addressed, use:
185
+ ### Shrine
186
+
187
+ Create a box
150
188
 
151
189
  ```ruby
152
- user = User.new
153
- user.attach(params[:avatar])
154
- user.save!
190
+ box = Lockbox.new(key: key)
155
191
  ```
156
192
 
157
- ### CarrierWave
193
+ Encrypt files before passing them to Shrine
158
194
 
159
- Add to your uploader:
195
+ ```ruby
196
+ LicenseUploader.upload(box.encrypt_io(file), :store)
197
+ ```
198
+
199
+ And decrypt them after reading
160
200
 
161
201
  ```ruby
162
- class LicenseUploader < CarrierWave::Uploader::Base
163
- encrypt
164
- end
202
+ box.decrypt(uploaded_file.read)
165
203
  ```
166
204
 
167
- Encryption is applied to all versions after processing.
205
+ For models, encrypt with:
206
+
207
+ ```ruby
208
+ license = params.require(:user).fetch(:license)
209
+ @user.license = box.encrypt_io(license)
210
+ ```
168
211
 
169
212
  To serve encrypted files, use a controller action.
170
213
 
171
214
  ```ruby
172
215
  def license
173
- send_data @user.license.read, type: @user.license.content_type
216
+ send_data box.decrypt(@user.license.read), type: @user.license.mime_type
174
217
  end
175
218
  ```
176
219
 
data/lib/lockbox.rb CHANGED
@@ -6,6 +6,8 @@ require "securerandom"
6
6
  require "lockbox/box"
7
7
  require "lockbox/encryptor"
8
8
  require "lockbox/key_generator"
9
+ require "lockbox/io"
10
+ require "lockbox/model"
9
11
  require "lockbox/utils"
10
12
  require "lockbox/version"
11
13
 
@@ -15,9 +17,12 @@ require "lockbox/railtie" if defined?(Rails)
15
17
 
16
18
  if defined?(ActiveSupport)
17
19
  ActiveSupport.on_load(:active_record) do
18
- require "lockbox/model"
19
20
  extend Lockbox::Model
20
21
  end
22
+
23
+ ActiveSupport.on_load(:mongoid) do
24
+ Mongoid::Document::ClassMethods.include(Lockbox::Model)
25
+ end
21
26
  end
22
27
 
23
28
  class Lockbox
@@ -49,35 +54,47 @@ class Lockbox
49
54
  attributes = fields.map { |_, v| v[:encrypted_attribute] }
50
55
  attributes += blind_indexes.map { |_, v| v[:bidx_attribute] }
51
56
 
52
- attributes.each_with_index do |attribute, i|
53
- relation =
54
- if i == 0
55
- relation.where(attribute => nil)
56
- else
57
- relation.or(model.where(attribute => nil))
58
- end
57
+ if defined?(ActiveRecord::Base) && model.is_a?(ActiveRecord::Base)
58
+ attributes.each_with_index do |attribute, i|
59
+ relation =
60
+ if i == 0
61
+ relation.where(attribute => nil)
62
+ else
63
+ relation.or(model.unscoped.where(attribute => nil))
64
+ end
65
+ end
59
66
  end
60
67
  end
61
68
 
62
- # migrate
63
- relation.find_each do |record|
64
- fields.each do |k, v|
65
- record.send("#{v[:attribute]}=", record.send(k)) if restart || !record.send(v[:encrypted_attribute])
69
+ if relation.respond_to?(:find_each)
70
+ relation.find_each do |record|
71
+ migrate_record(record, fields: fields, blind_indexes: blind_indexes, restart: restart)
66
72
  end
67
- blind_indexes.each do |k, v|
68
- record.send("compute_#{k}_bidx") if restart || !record.send(v[:bidx_attribute])
73
+ else
74
+ relation.all.each do |record|
75
+ migrate_record(record, fields: fields, blind_indexes: blind_indexes, restart: restart)
69
76
  end
70
- record.save(validate: false) if record.changed?
71
77
  end
72
78
  end
73
79
 
80
+ # private
81
+ def self.migrate_record(record, fields:, blind_indexes:, restart:)
82
+ fields.each do |k, v|
83
+ record.send("#{v[:attribute]}=", record.send(k)) if restart || !record.send(v[:encrypted_attribute])
84
+ end
85
+ blind_indexes.each do |k, v|
86
+ record.send("compute_#{k}_bidx") if restart || !record.send(v[:bidx_attribute])
87
+ end
88
+ record.save(validate: false) if record.changed?
89
+ end
90
+
74
91
  def initialize(**options)
75
92
  options = self.class.default_options.merge(options)
76
93
  previous_versions = options.delete(:previous_versions)
77
94
 
78
95
  @boxes =
79
96
  [Box.new(options)] +
80
- Array(previous_versions).map { |v| Box.new(v) }
97
+ Array(previous_versions).map { |v| Box.new({key: options[:key]}.merge(v)) }
81
98
  end
82
99
 
83
100
  def encrypt(message, **options)
@@ -112,6 +129,18 @@ class Lockbox
112
129
  end
113
130
  end
114
131
 
132
+ def encrypt_io(io, **options)
133
+ new_io = Lockbox::IO.new(encrypt(io.read, **options))
134
+ copy_metadata(io, new_io)
135
+ new_io
136
+ end
137
+
138
+ def decrypt_io(io, **options)
139
+ new_io = Lockbox::IO.new(decrypt(io.read, **options))
140
+ copy_metadata(io, new_io)
141
+ new_io
142
+ end
143
+
115
144
  def self.generate_key
116
145
  SecureRandom.hex(32)
117
146
  end
@@ -199,4 +228,14 @@ class Lockbox
199
228
  raise TypeError, "can't convert #{name} to string" unless str.respond_to?(:to_str)
200
229
  str.to_str
201
230
  end
231
+
232
+ def copy_metadata(source, target)
233
+ target.original_filename =
234
+ if source.respond_to?(:original_filename)
235
+ source.original_filename
236
+ elsif source.respond_to?(:path)
237
+ File.basename(source.path)
238
+ end
239
+ target.content_type = source.content_type if source.respond_to?(:content_type)
240
+ end
202
241
  end
@@ -10,35 +10,11 @@ class Lockbox
10
10
  def encrypted?
11
11
  # could use record_type directly
12
12
  # but record should already be loaded most of the time
13
- !Utils.encrypted_options(record, name).nil?
13
+ Utils.encrypted?(record, name)
14
14
  end
15
15
 
16
16
  def encrypt_attachable(attachable)
17
- options = Utils.encrypted_options(record, name)
18
- box = Utils.build_box(record, options, record.class.table_name, name)
19
-
20
- case attachable
21
- when ActiveStorage::Blob
22
- raise NotImplementedError, "Not supported"
23
- when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
24
- attachable = {
25
- io: StringIO.new(box.encrypt(attachable.read)),
26
- filename: attachable.original_filename,
27
- content_type: attachable.content_type
28
- }
29
- when Hash
30
- attachable = {
31
- io: StringIO.new(box.encrypt(attachable[:io].read)),
32
- filename: attachable[:filename],
33
- content_type: attachable[:content_type]
34
- }
35
- when String
36
- raise NotImplementedError, "Not supported"
37
- else
38
- nil
39
- end
40
-
41
- attachable
17
+ Utils.encrypt_attachable(record, name, attachable)
42
18
  end
43
19
 
44
20
  def rebuild_attachable(attachment)
@@ -51,9 +27,11 @@ class Lockbox
51
27
  end
52
28
 
53
29
  module AttachedOne
54
- def attach(attachable)
55
- attachable = encrypt_attachable(attachable) if encrypted?
56
- super(attachable)
30
+ if ActiveStorage::VERSION::MAJOR < 6
31
+ def attach(attachable)
32
+ attachable = encrypt_attachable(attachable) if encrypted?
33
+ super(attachable)
34
+ end
57
35
  end
58
36
 
59
37
  def rotate_encryption!
@@ -66,15 +44,17 @@ class Lockbox
66
44
  end
67
45
 
68
46
  module AttachedMany
69
- def attach(*attachables)
70
- if encrypted?
71
- attachables =
72
- attachables.flatten.collect do |attachable|
73
- encrypt_attachable(attachable)
74
- end
75
- end
47
+ if ActiveStorage::VERSION::MAJOR < 6
48
+ def attach(*attachables)
49
+ if encrypted?
50
+ attachables =
51
+ attachables.flatten.collect do |attachable|
52
+ encrypt_attachable(attachable)
53
+ end
54
+ end
76
55
 
77
- super(attachables)
56
+ super(attachables)
57
+ end
78
58
  end
79
59
 
80
60
  def rotate_encryption!
@@ -99,6 +79,14 @@ class Lockbox
99
79
  end
100
80
  end
101
81
 
82
+ module CreateOne
83
+ def initialize(name, record, attachable)
84
+ # this won't encrypt existing blobs
85
+ attachable = Lockbox::Utils.encrypt_attachable(record, name, attachable) if Lockbox::Utils.encrypted?(record, name) && !attachable.is_a?(ActiveStorage::Blob)
86
+ super(name, record, attachable)
87
+ end
88
+ end
89
+
102
90
  module Attachment
103
91
  extend ActiveSupport::Concern
104
92
 
@@ -1,15 +1,11 @@
1
1
  class Lockbox
2
2
  module CarrierWaveExtensions
3
- class FileIO < StringIO
4
- attr_accessor :original_filename
5
- end
6
-
7
3
  def encrypt(**options)
8
4
  class_eval do
9
5
  before :cache, :encrypt
10
6
 
11
7
  def encrypt(file)
12
- @file = CarrierWave::SanitizedFile.new(StringIO.new(lockbox.encrypt(file.read)))
8
+ @file = CarrierWave::SanitizedFile.new(lockbox.encrypt_io(file))
13
9
  end
14
10
 
15
11
  def read
@@ -22,7 +18,7 @@ class Lockbox
22
18
  end
23
19
 
24
20
  def rotate_encryption!
25
- io = FileIO.new(read)
21
+ io = Lockbox::IO.new(read)
26
22
  io.original_filename = file.filename
27
23
  previous_value = enable_processing
28
24
  begin
data/lib/lockbox/io.rb ADDED
@@ -0,0 +1,5 @@
1
+ class Lockbox
2
+ class IO < StringIO
3
+ attr_accessor :original_filename, :content_type
4
+ end
5
+ end
data/lib/lockbox/model.rb CHANGED
@@ -107,7 +107,7 @@ class Lockbox
107
107
  def serializable_hash(options = nil)
108
108
  options = options.try(:dup) || {}
109
109
  options[:except] = Array(options[:except])
110
- options[:except] += self.class.lockbox_attributes.values.reject { |v| v[:attached] }.flat_map { |v| [v[:attribute], v[:encrypted_attribute]] }
110
+ options[:except] += self.class.lockbox_attributes.values.flat_map { |v| [v[:attribute], v[:encrypted_attribute]] }
111
111
  super(options)
112
112
  end
113
113
 
@@ -127,9 +127,21 @@ class Lockbox
127
127
  self.class.lockbox_attributes.each do |_, lockbox_attribute|
128
128
  attribute = lockbox_attribute[:attribute]
129
129
 
130
- if changes.include?(attribute) && self.class.attribute_types[attribute].is_a?(ActiveRecord::Type::Serialized)
131
- send("#{attribute}=", send(attribute))
130
+ if changes.include?(attribute)
131
+ type = (self.class.try(:attribute_types) || {})[attribute]
132
+ if type && type.is_a?(ActiveRecord::Type::Serialized)
133
+ send("#{attribute}=", send(attribute))
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ if defined?(Mongoid::Document) && included_modules.include?(Mongoid::Document)
140
+ def reload
141
+ self.class.lockbox_attributes.each do |_, v|
142
+ instance_variable_set("@#{v[:attribute]}", nil)
132
143
  end
144
+ super
133
145
  end
134
146
  end
135
147
  end
@@ -137,7 +149,36 @@ class Lockbox
137
149
  serialize name, JSON if options[:type] == :json
138
150
  serialize name, Hash if options[:type] == :hash
139
151
 
140
- attribute name, attribute_type
152
+ if respond_to?(:attribute)
153
+ attribute name, attribute_type
154
+ else
155
+ m = Module.new do
156
+ define_method("#{name}=") do |val|
157
+ prev_val = instance_variable_get("@#{name}")
158
+
159
+ unless val == prev_val
160
+ # custom attribute_will_change! method
161
+ unless changed_attributes.key?(name.to_s)
162
+ changed_attributes[name.to_s] = prev_val.__deep_copy__
163
+ end
164
+ end
165
+
166
+ instance_variable_set("@#{name}", val)
167
+ end
168
+
169
+ define_method(name) do
170
+ instance_variable_get("@#{name}")
171
+ end
172
+ end
173
+
174
+ include m
175
+
176
+ alias_method "#{name}_changed?", "#{encrypted_attribute}_changed?"
177
+
178
+ define_method "#{name}_was" do
179
+ attribute_was(name.to_s)
180
+ end
181
+ end
141
182
 
142
183
  define_method("#{name}=") do |message|
143
184
  original_message = message
@@ -174,8 +215,8 @@ class Lockbox
174
215
  # do nothing
175
216
  # encrypt will convert to binary
176
217
  else
177
- type = self.class.attribute_types[name.to_s]
178
- if type.is_a?(ActiveRecord::Type::Serialized)
218
+ type = (self.class.try(:attribute_types) || {})[name.to_s]
219
+ if type && type.is_a?(ActiveRecord::Type::Serialized)
179
220
  message = type.serialize(message)
180
221
  end
181
222
  end
@@ -195,6 +236,7 @@ class Lockbox
195
236
  else
196
237
  self.class.send(class_method_name, message, context: self)
197
238
  end
239
+
198
240
  send("#{encrypted_attribute}=", ciphertext)
199
241
 
200
242
  super(original_message)
@@ -210,7 +252,8 @@ class Lockbox
210
252
  ciphertext
211
253
  else
212
254
  ciphertext = Base64.decode64(ciphertext) if encode
213
- Lockbox::Utils.build_box(self, options, self.class.table_name, encrypted_attribute).decrypt(ciphertext)
255
+ table = self.class.respond_to?(:table_name) ? self.class.table_name : self.class.collection_name.to_s
256
+ Lockbox::Utils.build_box(self, options, table, encrypted_attribute).decrypt(ciphertext)
214
257
  end
215
258
 
216
259
  unless message.nil?
@@ -233,8 +276,8 @@ class Lockbox
233
276
  # do nothing
234
277
  # decrypt returns binary string
235
278
  else
236
- type = self.class.attribute_types[name.to_s]
237
- if type.is_a?(ActiveRecord::Type::Serialized)
279
+ type = (self.class.try(:attribute_types) || {})[name.to_s]
280
+ if type && type.is_a?(ActiveRecord::Type::Serialized)
238
281
  message = type.deserialize(message)
239
282
  else
240
283
  # default to string if not serialized
@@ -244,13 +287,15 @@ class Lockbox
244
287
  end
245
288
 
246
289
  # set previous attribute on first decrypt
247
- @attributes[name.to_s].instance_variable_set("@value_before_type_cast", message)
290
+ @attributes[name.to_s].instance_variable_set("@value_before_type_cast", message) if @attributes[name.to_s]
248
291
 
249
292
  # cache
250
293
  if respond_to?(:_write_attribute, true)
251
294
  _write_attribute(name, message)
252
- else
295
+ elsif respond_to?(:raw_write_attribute)
253
296
  raw_write_attribute(name, message)
297
+ else
298
+ instance_variable_set("@#{name}", message)
254
299
  end
255
300
  end
256
301
 
@@ -259,7 +304,8 @@ class Lockbox
259
304
 
260
305
  # for fixtures
261
306
  define_singleton_method class_method_name do |message, **opts|
262
- ciphertext = Lockbox::Utils.build_box(opts[:context], options, table_name, encrypted_attribute).encrypt(message)
307
+ table = respond_to?(:table_name) ? table_name : collection_name.to_s
308
+ ciphertext = Lockbox::Utils.build_box(opts[:context], options, table, encrypted_attribute).encrypt(message)
263
309
  ciphertext = Base64.strict_encode64(ciphertext) if encode
264
310
  ciphertext
265
311
  end
@@ -6,6 +6,9 @@ class Lockbox
6
6
  if defined?(ActiveStorage)
7
7
  require "lockbox/active_storage_extensions"
8
8
  ActiveStorage::Attached.prepend(Lockbox::ActiveStorageExtensions::Attached)
9
+ if ActiveStorage::VERSION::MAJOR >= 6
10
+ ActiveStorage::Attached::Changes::CreateOne.prepend(Lockbox::ActiveStorageExtensions::CreateOne)
11
+ end
9
12
  ActiveStorage::Attached::One.prepend(Lockbox::ActiveStorageExtensions::AttachedOne)
10
13
  ActiveStorage::Attached::Many.prepend(Lockbox::ActiveStorageExtensions::AttachedMany)
11
14
  end
data/lib/lockbox/utils.rb CHANGED
@@ -27,5 +27,37 @@ class Lockbox
27
27
  end
28
28
  key
29
29
  end
30
+
31
+ def self.encrypted?(record, name)
32
+ !encrypted_options(record, name).nil?
33
+ end
34
+
35
+ def self.encrypt_attachable(record, name, attachable)
36
+ options = encrypted_options(record, name)
37
+ box = build_box(record, options, record.class.table_name, name)
38
+
39
+ case attachable
40
+ when ActiveStorage::Blob
41
+ raise NotImplementedError, "Not supported"
42
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
43
+ attachable = {
44
+ io: StringIO.new(box.encrypt(attachable.read)),
45
+ filename: attachable.original_filename,
46
+ content_type: attachable.content_type
47
+ }
48
+ when Hash
49
+ attachable = {
50
+ io: StringIO.new(box.encrypt(attachable[:io].read)),
51
+ filename: attachable[:filename],
52
+ content_type: attachable[:content_type]
53
+ }
54
+ when String
55
+ raise NotImplementedError, "Not supported"
56
+ else
57
+ nil
58
+ end
59
+
60
+ attachable
61
+ end
30
62
  end
31
63
  end
@@ -1,3 +1,3 @@
1
1
  class Lockbox
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lockbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-31 00:00:00.000000000 Z
11
+ date: 2019-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: mongoid
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  description:
168
182
  email: andrew@chartkick.com
169
183
  executables: []
@@ -179,6 +193,7 @@ files:
179
193
  - lib/lockbox/box.rb
180
194
  - lib/lockbox/carrier_wave_extensions.rb
181
195
  - lib/lockbox/encryptor.rb
196
+ - lib/lockbox/io.rb
182
197
  - lib/lockbox/key_generator.rb
183
198
  - lib/lockbox/model.rb
184
199
  - lib/lockbox/railtie.rb