lockbox 0.2.3 → 0.2.4

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