lockbox 1.3.0 → 1.4.1

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: db8c162439dc5376d1aabf48af3925fd2d7a5129e3902b49b973dce9eda16a77
4
- data.tar.gz: 2e82dc5026e09fdaee0bb1baddabc9b0f8294f38cf613ee09368d62aa39c4ff4
3
+ metadata.gz: b4771553bf23214b514e9a4a5c94ce665d234d34969f9a3941ea68bdc6729ed4
4
+ data.tar.gz: a921894d4f3f43d5245a91941e06f79ea652f09c9c77ccf8dc7e442d1dbda0e5
5
5
  SHA512:
6
- metadata.gz: e8d6d9a2c4661767c01ab8874f29c3fd705712d4f3d6d153e09b4c7ad6441bb0812d45c6cf526d3a59177ae23c44289b1aa375e462303c17fcb952edd4641a8e
7
- data.tar.gz: 2e5cd80ddca65447f10a666b5568bbdeff449bf6517c944a67f6fdfdd5ff253e7560569133216a5459d1519ad3bb72b5cabd17065240a2aa1091750e21b4c26c
6
+ metadata.gz: 90fef82d743a0172a61728fd9f314eca597d43820e87aeb9cb77330e4ec824919d354b81cf4c6c9c2b1c8cedcebb8916425fed5e469b24ea11b6a2e8730feb6f
7
+ data.tar.gz: fe874ec6aa3f563927f2ea1743d34a795468846ce74cc9e1a0467e41757721d40c8449d7093b158f63f92b8566ea578cf88e5f2b0a8caefc9facee2a8b731afa
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## 1.4.1 (2024-09-09)
2
+
3
+ - Fixed error message for previews for Active Storage 7.1.4
4
+
5
+ ## 1.4.0 (2024-08-09)
6
+
7
+ - Added support for Active Record 7.2
8
+ - Added support for Mongoid 9
9
+ - Fixed error when `decryption_key` option is a proc or symbol and returns `nil`
10
+
11
+ ## 1.3.3 (2024-02-07)
12
+
13
+ - Added warning for encrypting store attributes
14
+
15
+ ## 1.3.2 (2024-01-10)
16
+
17
+ - Fixed issue with serialized attributes
18
+
19
+ ## 1.3.1 (2024-01-06)
20
+
21
+ - Fixed error with `array` and `hash` types and no default column serializer with Rails 7.1
22
+ - Fixed Action Text deserialization with Rails 7.1
23
+
1
24
  ## 1.3.0 (2023-07-02)
2
25
 
3
26
  - Added support for CarrierWave 3
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018-2022 Andrew Kane
3
+ Copyright (c) 2018-2024 Andrew Kane
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  Learn [the principles behind it](https://ankane.org/modern-encryption-rails), [how to secure emails with Devise](https://ankane.org/securing-user-emails-lockbox), and [how to secure sensitive data in Rails](https://ankane.org/sensitive-data-rails).
11
11
 
12
- [![Build Status](https://github.com/ankane/lockbox/workflows/build/badge.svg?branch=master)](https://github.com/ankane/lockbox/actions)
12
+ [![Build Status](https://github.com/ankane/lockbox/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/lockbox/actions)
13
13
 
14
14
  ## Installation
15
15
 
@@ -72,7 +72,7 @@ Then follow the instructions below for the data you want to encrypt.
72
72
  Create a migration with:
73
73
 
74
74
  ```ruby
75
- class AddEmailCiphertextToUsers < ActiveRecord::Migration[7.0]
75
+ class AddEmailCiphertextToUsers < ActiveRecord::Migration[7.2]
76
76
  def change
77
77
  add_column :users, :email_ciphertext, :text
78
78
  end
@@ -140,6 +140,8 @@ class User < ApplicationRecord
140
140
  end
141
141
  ```
142
142
 
143
+ For [Active Record Store](https://api.rubyonrails.org/classes/ActiveRecord/Store.html), encrypt the column rather than individual accessors.
144
+
143
145
  For [StoreModel](https://github.com/DmitryTsepelev/store_model), use:
144
146
 
145
147
  ```ruby
@@ -192,7 +194,7 @@ class User < ApplicationRecord
192
194
  has_encrypted :email
193
195
 
194
196
  # remove this line after dropping email column
195
- self.ignored_columns = ["email"]
197
+ self.ignored_columns += ["email"]
196
198
  end
197
199
  ```
198
200
 
@@ -249,7 +251,7 @@ User.decrypt_email_ciphertext(user.email_ciphertext)
249
251
  Create a migration with:
250
252
 
251
253
  ```ruby
252
- class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[7.0]
254
+ class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[7.2]
253
255
  def change
254
256
  add_column :action_text_rich_texts, :body_ciphertext, :text
255
257
  end
@@ -380,7 +382,7 @@ Encryption is applied to all versions after processing.
380
382
  You can mount the uploader [as normal](https://github.com/carrierwaveuploader/carrierwave#activerecord). With Active Record, this involves creating a migration:
381
383
 
382
384
  ```ruby
383
- class AddLicenseToUsers < ActiveRecord::Migration[7.0]
385
+ class AddLicenseToUsers < ActiveRecord::Migration[7.2]
384
386
  def change
385
387
  add_column :users, :license, :string
386
388
  end
@@ -908,7 +910,7 @@ end
908
910
  You can use `binary` columns for the ciphertext instead of `text` columns.
909
911
 
910
912
  ```ruby
911
- class AddEmailCiphertextToUsers < ActiveRecord::Migration[7.0]
913
+ class AddEmailCiphertextToUsers < ActiveRecord::Migration[7.2]
912
914
  def change
913
915
  add_column :users, :email_ciphertext, :binary
914
916
  end
@@ -959,7 +961,7 @@ end
959
961
  Create a migration with:
960
962
 
961
963
  ```ruby
962
- class MigrateToLockbox < ActiveRecord::Migration[7.0]
964
+ class MigrateToLockbox < ActiveRecord::Migration[7.2]
963
965
  def change
964
966
  add_column :users, :name_ciphertext, :text
965
967
  add_column :users, :email_ciphertext, :text
@@ -992,7 +994,7 @@ end
992
994
  Then remove the previous gem from your Gemfile and drop its columns.
993
995
 
994
996
  ```ruby
995
- class RemovePreviousEncryptedColumns < ActiveRecord::Migration[7.0]
997
+ class RemovePreviousEncryptedColumns < ActiveRecord::Migration[7.2]
996
998
  def change
997
999
  remove_column :users, :encrypted_name, :text
998
1000
  remove_column :users, :encrypted_name_iv, :text
@@ -1014,21 +1016,6 @@ class User < ApplicationRecord
1014
1016
  end
1015
1017
  ```
1016
1018
 
1017
- ### 0.6.0
1018
-
1019
- 0.6.0 adds `encrypted: true` to Active Storage metadata for new files. This field is informational, but if you prefer to add it to existing files, use:
1020
-
1021
- ```ruby
1022
- User.with_attached_license.find_each do |user|
1023
- next unless user.license.attached?
1024
-
1025
- metadata = user.license.metadata
1026
- unless metadata["encrypted"]
1027
- user.license.blob.update!(metadata: metadata.merge("encrypted" => true))
1028
- end
1029
- end
1030
- ```
1031
-
1032
1019
  ## History
1033
1020
 
1034
1021
  View the [changelog](https://github.com/ankane/lockbox/blob/master/CHANGELOG.md)
@@ -124,6 +124,13 @@ module Lockbox
124
124
  super
125
125
  end
126
126
 
127
+ if ActiveStorage::VERSION::STRING.to_f == 7.1 && ActiveStorage.version >= "7.1.4"
128
+ def transform_variants_later
129
+ blob.instance_variable_set(:@lockbox_encrypted, true) if Utils.encrypted_options(record, name)
130
+ super
131
+ end
132
+ end
133
+
127
134
  if ActiveStorage::VERSION::MAJOR >= 6
128
135
  def open(**options)
129
136
  blob.open(**options) do |file|
@@ -150,6 +157,12 @@ module Lockbox
150
157
  end
151
158
 
152
159
  module Blob
160
+ if ActiveStorage::VERSION::STRING.to_f == 7.1 && ActiveStorage.version >= "7.1.4"
161
+ def preview_image_needed_before_processing_variants?
162
+ !instance_variable_defined?(:@lockbox_encrypted) && super
163
+ end
164
+ end
165
+
153
166
  private
154
167
 
155
168
  def extract_content_type(io)
@@ -79,7 +79,7 @@ module Lockbox
79
79
  while uploader.parent_version
80
80
  uploader = uploader.parent_version
81
81
  end
82
- uploader.class.name.sub(/Uploader\z/, "").underscore
82
+ uploader.class.name.delete_suffix("Uploader").underscore
83
83
  end
84
84
  end
85
85
 
@@ -2,7 +2,7 @@ module Lockbox
2
2
  class Migrator
3
3
  def initialize(relation, batch_size:)
4
4
  @relation = relation
5
- @transaction = @relation.respond_to?(:transaction)
5
+ @transaction = @relation.respond_to?(:transaction) && !mongoid_relation?(base_relation)
6
6
  @batch_size = batch_size
7
7
  end
8
8
 
data/lib/lockbox/model.rb CHANGED
@@ -137,13 +137,16 @@ module Lockbox
137
137
  # essentially a no-op if already loaded
138
138
  # an exception is thrown if decryption fails
139
139
  self.class.lockbox_attributes.each do |_, lockbox_attribute|
140
- # don't try to decrypt if no decryption key given
141
- next if lockbox_attribute[:algorithm] == "hybrid" && lockbox_attribute[:decryption_key].nil?
142
-
143
140
  # it is possible that the encrypted attribute is not loaded, eg.
144
141
  # if the record was fetched partially (`User.select(:id).first`).
145
142
  # accessing a not loaded attribute raises an `ActiveModel::MissingAttributeError`.
146
- send(lockbox_attribute[:attribute]) if has_attribute?(lockbox_attribute[:encrypted_attribute])
143
+ if has_attribute?(lockbox_attribute[:encrypted_attribute])
144
+ begin
145
+ send(lockbox_attribute[:attribute])
146
+ rescue ArgumentError => e
147
+ raise e if e.message != "No decryption key set"
148
+ end
149
+ end
147
150
  end
148
151
  super
149
152
  end
@@ -230,6 +233,20 @@ module Lockbox
230
233
  end
231
234
 
232
235
  if ActiveRecord::VERSION::MAJOR >= 6
236
+ if ActiveRecord::VERSION::STRING.to_f >= 7.2
237
+ def self.insert(attributes, **options)
238
+ super(lockbox_map_record_attributes(attributes), **options)
239
+ end
240
+
241
+ def self.insert!(attributes, **options)
242
+ super(lockbox_map_record_attributes(attributes), **options)
243
+ end
244
+
245
+ def self.upsert(attributes, **options)
246
+ super(lockbox_map_record_attributes(attributes, check_readonly: true), **options)
247
+ end
248
+ end
249
+
233
250
  def self.insert_all(attributes, **options)
234
251
  super(lockbox_map_attributes(attributes), **options)
235
252
  end
@@ -248,30 +265,37 @@ module Lockbox
248
265
  return records unless records.is_a?(Array)
249
266
 
250
267
  records.map do |attributes|
251
- # transform keys like Active Record
252
- attributes = attributes.transform_keys do |key|
253
- n = key.to_s
254
- attribute_aliases[n] || n
255
- end
268
+ lockbox_map_record_attributes(attributes, check_readonly: false)
269
+ end
270
+ end
256
271
 
257
- lockbox_attributes = self.lockbox_attributes.slice(*attributes.keys.map(&:to_sym))
258
- lockbox_attributes.each do |key, lockbox_attribute|
259
- attribute = key.to_s
260
- # check read only
261
- # users should mark both plaintext and ciphertext columns
262
- if check_readonly && readonly_attributes.include?(attribute) && !readonly_attributes.include?(lockbox_attribute[:encrypted_attribute].to_s)
263
- warn "[lockbox] WARNING: Mark attribute as readonly: #{lockbox_attribute[:encrypted_attribute]}"
264
- end
265
-
266
- message = attributes[attribute]
267
- attributes.delete(attribute) unless lockbox_attribute[:migrating]
268
- encrypted_attribute = lockbox_attribute[:encrypted_attribute]
269
- ciphertext = send("generate_#{encrypted_attribute}", message)
270
- attributes[encrypted_attribute] = ciphertext
272
+ # private
273
+ def self.lockbox_map_record_attributes(attributes, check_readonly: false)
274
+ return attributes unless attributes.is_a?(Hash)
275
+
276
+ # transform keys like Active Record
277
+ attributes = attributes.transform_keys do |key|
278
+ n = key.to_s
279
+ attribute_aliases[n] || n
280
+ end
281
+
282
+ lockbox_attributes = self.lockbox_attributes.slice(*attributes.keys.map(&:to_sym))
283
+ lockbox_attributes.each do |key, lockbox_attribute|
284
+ attribute = key.to_s
285
+ # check read only
286
+ # users should mark both plaintext and ciphertext columns
287
+ if check_readonly && readonly_attributes.include?(attribute) && !readonly_attributes.include?(lockbox_attribute[:encrypted_attribute].to_s)
288
+ warn "[lockbox] WARNING: Mark attribute as readonly: #{lockbox_attribute[:encrypted_attribute]}"
271
289
  end
272
290
 
273
- attributes
291
+ message = attributes[attribute]
292
+ attributes.delete(attribute) unless lockbox_attribute[:migrating]
293
+ encrypted_attribute = lockbox_attribute[:encrypted_attribute]
294
+ ciphertext = send("generate_#{encrypted_attribute}", message)
295
+ attributes[encrypted_attribute] = ciphertext
274
296
  end
297
+
298
+ attributes
275
299
  end
276
300
  end
277
301
  else
@@ -289,8 +313,18 @@ module Lockbox
289
313
  @lockbox_attributes[original_name] = options
290
314
 
291
315
  if activerecord
316
+ # warn on store attributes
317
+ if stored_attributes.any? { |k, v| v.include?(name) }
318
+ warn "[lockbox] WARNING: encrypting store accessors is not supported. Encrypt the column instead."
319
+ end
320
+
292
321
  # warn on default attributes
293
- if attributes_to_define_after_schema_loads.key?(name.to_s)
322
+ if ActiveRecord::VERSION::STRING.to_f >= 7.2
323
+ # TODO improve
324
+ if pending_attribute_modifications.any? { |v| v.is_a?(ActiveModel::AttributeRegistration::ClassMethods::PendingDefault) && v.name == name.to_s }
325
+ warn "[lockbox] WARNING: attributes with `:default` option are not supported. Use `after_initialize` instead."
326
+ end
327
+ elsif attributes_to_define_after_schema_loads.key?(name.to_s)
294
328
  opt = attributes_to_define_after_schema_loads[name.to_s][1]
295
329
 
296
330
  has_default =
@@ -324,13 +358,43 @@ module Lockbox
324
358
  attribute name, attribute_type
325
359
 
326
360
  if ActiveRecord::VERSION::STRING.to_f >= 7.1
327
- serialize name, coder: JSON if options[:type] == :json
328
- serialize name, type: Hash if options[:type] == :hash
329
- serialize name, type: Array if options[:type] == :array
361
+ case options[:type]
362
+ when :json
363
+ serialize name, coder: JSON
364
+ when :hash
365
+ serialize name, type: Hash, coder: default_column_serializer || YAML
366
+ when :array
367
+ serialize name, type: Array, coder: default_column_serializer || YAML
368
+ end
330
369
  else
331
- serialize name, JSON if options[:type] == :json
332
- serialize name, Hash if options[:type] == :hash
333
- serialize name, Array if options[:type] == :array
370
+ case options[:type]
371
+ when :json
372
+ serialize name, JSON
373
+ when :hash
374
+ serialize name, Hash
375
+ when :array
376
+ serialize name, Array
377
+ end
378
+ end
379
+ elsif ActiveRecord::VERSION::STRING.to_f >= 7.2
380
+ decorate_attributes([name]) do |attr_name, cast_type|
381
+ if cast_type.instance_of?(ActiveRecord::Type::Value)
382
+ original_type = pending_attribute_modifications.find { |v| v.is_a?(ActiveModel::AttributeRegistration::ClassMethods::PendingType) && v.name == original_name.to_s && !v.type.nil? }&.type
383
+ if original_type
384
+ original_type
385
+ elsif options[:migrating]
386
+ cast_type
387
+ else
388
+ ActiveRecord::Type::String.new
389
+ end
390
+ elsif cast_type.is_a?(ActiveRecord::Type::Serialized) && cast_type.subtype.instance_of?(ActiveModel::Type::Value)
391
+ # hack to set string type after serialize
392
+ # otherwise, type gets set to ActiveModel::Type::Value
393
+ # which always returns false for changed_in_place?
394
+ ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, cast_type.coder)
395
+ else
396
+ cast_type
397
+ end
334
398
  end
335
399
  elsif !attributes_to_define_after_schema_loads.key?(name.to_s)
336
400
  # when migrating it's best to specify the type directly
@@ -345,8 +409,7 @@ module Lockbox
345
409
  attribute name, :string
346
410
  end
347
411
  else
348
- # hack for Active Record 6.1
349
- # to set string type after serialize
412
+ # hack for Active Record 6.1+ to set string type after serialize
350
413
  # otherwise, type gets set to ActiveModel::Type::Value
351
414
  # which always returns false for changed_in_place?
352
415
  # earlier versions of Active Record take the previous code path
@@ -432,12 +495,12 @@ module Lockbox
432
495
  # decrypt first for dirty tracking
433
496
  # don't raise error if can't decrypt previous
434
497
  # don't try to decrypt if no decryption key given
435
- unless options[:algorithm] == "hybrid" && options[:decryption_key].nil?
436
- begin
437
- send(name)
438
- rescue Lockbox::DecryptionError
439
- warn "[lockbox] Decrypting previous value failed"
440
- end
498
+ begin
499
+ send(name)
500
+ rescue Lockbox::DecryptionError
501
+ warn "[lockbox] Decrypting previous value failed"
502
+ rescue ArgumentError => e
503
+ raise e if e.message != "No decryption key set"
441
504
  end
442
505
 
443
506
  send("lockbox_direct_#{name}=", message)
@@ -499,6 +562,9 @@ module Lockbox
499
562
  clear_attribute_change(name)
500
563
  end
501
564
  end
565
+
566
+ # ensure same object is returned as next call
567
+ message = super()
502
568
  else
503
569
  instance_variable_set("@#{name}", message)
504
570
  end
@@ -615,6 +681,10 @@ module Lockbox
615
681
  else
616
682
  # use original name for serialized attributes if no type specified
617
683
  type = (try(:attribute_types) || {})[(options[:type] ? name : original_name).to_s]
684
+ # for Action Text
685
+ if activerecord && type.is_a?(ActiveRecord::Type::Serialized) && defined?(ActionText::Content) && type.coder == ActionText::Content
686
+ message.force_encoding(Encoding::UTF_8)
687
+ end
618
688
  message = type.deserialize(message) if type
619
689
  message.force_encoding(Encoding::UTF_8) if !type || type.is_a?(ActiveModel::Type::String)
620
690
  end
@@ -647,7 +717,8 @@ module Lockbox
647
717
  end
648
718
 
649
719
  def lockbox_encrypts(*attributes, **options)
650
- ActiveSupport::Deprecation.warn("`#{__callee__}` is deprecated in favor of `has_encrypted`")
720
+ deprecator = ActiveSupport::VERSION::STRING.to_f >= 7.2 ? ActiveSupport.deprecator : ActiveSupport::Deprecation
721
+ deprecator.warn("`#{__callee__}` is deprecated in favor of `has_encrypted`")
651
722
  has_encrypted(*attributes, **options)
652
723
  end
653
724
 
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "1.3.0"
2
+ VERSION = "1.4.1"
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: 1.3.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-02 00:00:00.000000000 Z
11
+ date: 2024-09-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: andrew@ankane.org
@@ -58,7 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
58
  - !ruby/object:Gem::Version
59
59
  version: '0'
60
60
  requirements: []
61
- rubygems_version: 3.4.10
61
+ rubygems_version: 3.5.16
62
62
  signing_key:
63
63
  specification_version: 4
64
64
  summary: Modern encryption for Ruby and Rails