lockbox 0.4.6 → 0.4.7

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: 2fb82c6baf0c56ae7f051c8363f7cb52554ebab8180b891d4aa25caf7505cec3
4
- data.tar.gz: 64b6bda4259cc3fddd7e5635333b0b7c4a6680c4ff2da31bba735603ea50b010
3
+ metadata.gz: 3f8c447dd90537203a1a3038c347c10de0e48f5b29795b382a2a77019e6e5764
4
+ data.tar.gz: 9133e9eb0c2132b7c77c39f8c24a3c27ea9b3cbb1d3d82f7f069b2db9992198f
5
5
  SHA512:
6
- metadata.gz: e1953d9159c2cb1f1ec55436d925738777ea0b78afedcb32864280f7772d1d9e12a48f65c69385fd6bcdab166668049b3889f1e17e3b316c696705bcb98650dd
7
- data.tar.gz: a3559c399a385949526137eed03b73c38baae424bed3b597c89f287222e6e18fd2c5665b2cbba6b20dad71bf680f0840ee2753a68befd3af40fdb522e45e4088
6
+ metadata.gz: 4396ee4ead0de0592e7a3574b563f98d58f0402198dd8662cbdc374cc5f8a39e629f6397414b498456c665e8a635518db3a532056ecd42154206fde4ab938e5c
7
+ data.tar.gz: 6057ea6f43db261580a0ee0ae13f1303251d2ee8ef2b1bcdba8399b6a858dca1fdf6c84e25f5da85a059a6d260be7094969e2118ff11a8c1d2565617c48f74cd
@@ -1,3 +1,8 @@
1
+ ## 0.4.7 (2020-08-18)
2
+
3
+ - Added `lockbox_options` method to encrypted CarrierWave uploaders
4
+ - Improved attribute loading when no decryption key specified
5
+
1
6
  ## 0.4.6 (2020-07-02)
2
7
 
3
8
  - Added support for `update_column` and `update_columns`
data/README.md CHANGED
@@ -190,6 +190,8 @@ end
190
190
 
191
191
  ## Action Text
192
192
 
193
+ **Note:** Action Text uses direct uploads for files, which cannot be encrypted with application-level encryption like Lockbox. This only encrypts the database field.
194
+
193
195
  Create a migration with:
194
196
 
195
197
  ```ruby
@@ -264,8 +266,9 @@ end
264
266
 
265
267
  There are a few limitations to be aware of:
266
268
 
267
- - Metadata like image width and height are not extracted when encrypted
268
- - Direct uploads cannot be encrypted
269
+ - Variants and previews aren’t supported when encrypted
270
+ - Metadata like image width and height aren’t extracted when encrypted
271
+ - Direct uploads can’t be encrypted with application-level encryption like Lockbox, but can use server-side encryption
269
272
 
270
273
  To serve encrypted files, use a controller action.
271
274
 
@@ -508,6 +511,24 @@ Lockbox.rotate(User, attributes: [:email])
508
511
 
509
512
  Once all records are rotated, you can remove `previous_versions` from the model.
510
513
 
514
+ ### Action Text
515
+
516
+ Update your initializer:
517
+
518
+ ```ruby
519
+ Lockbox.encrypts_action_text_body(previous_versions: [{key: previous_key}])
520
+ ```
521
+
522
+ Use `master_key` instead of `key` if passing the master key.
523
+
524
+ To rotate existing records, use:
525
+
526
+ ```ruby
527
+ Lockbox.rotate(ActionText::RichText, attributes: [:body])
528
+ ```
529
+
530
+ Once all records are rotated, you can remove `previous_versions` from the initializer.
531
+
511
532
  ### Active Storage
512
533
 
513
534
  Update your model:
@@ -550,6 +571,14 @@ User.find_each do |user|
550
571
  end
551
572
  ```
552
573
 
574
+ For multiple files, use:
575
+
576
+ ```ruby
577
+ User.find_each do |user|
578
+ user.licenses.map(&:rotate_encryption!)
579
+ end
580
+ ```
581
+
553
582
  Once all files are rotated, you can remove `previous_versions` from the model.
554
583
 
555
584
  ### Local Files & Strings
@@ -734,12 +763,32 @@ end
734
763
 
735
764
  You can use a key management service to manage your keys with [KMS Encrypted](https://github.com/ankane/kms_encrypted).
736
765
 
766
+ For Active Record and Mongoid, use:
767
+
737
768
  ```ruby
738
769
  class User < ApplicationRecord
739
770
  encrypts :email, key: :kms_key
740
771
  end
741
772
  ```
742
773
 
774
+ For Action Text, use:
775
+
776
+ ```ruby
777
+ ActiveSupport.on_load(:action_text_rich_text) do
778
+ ActionText::RichText.has_kms_key
779
+ end
780
+
781
+ Lockbox.encrypts_action_text_body(key: :kms_key)
782
+ ```
783
+
784
+ For Active Storage, use:
785
+
786
+ ```ruby
787
+ class User < ApplicationRecord
788
+ encrypts_attached :license, key: :kms_key
789
+ end
790
+ ```
791
+
743
792
  For CarrierWave, use:
744
793
 
745
794
  ```ruby
@@ -772,7 +821,7 @@ lockbox.encrypt("clear").bytesize # 44
772
821
  lockbox.encrypt("consider").bytesize # 44
773
822
  ```
774
823
 
775
- The block size for padding is 16 bytes by default. If we have a status larger than 15 bytes, it will have a different length than the others.
824
+ The block size for padding is 16 bytes by default. Lockbox uses [ISO/IEC 7816-4](https://en.wikipedia.org/wiki/Padding_(cryptography)#ISO/IEC_7816-4) padding, which uses at least one byte, so if we have a status larger than 15 bytes, it will have a different length than the others.
776
825
 
777
826
  ```ruby
778
827
  box.encrypt("length15status!").bytesize # 44
@@ -785,9 +834,25 @@ Change the block size with:
785
834
  Lockbox.new(padding: 32) # bytes
786
835
  ```
787
836
 
837
+ ## Associated Data
838
+
839
+ You can pass extra context during encryption to make sure encrypted data isn’t moved to a different context.
840
+
841
+ ```ruby
842
+ lockbox = Lockbox.new(key: key)
843
+ ciphertext = lockbox.encrypt(message, associated_data: "somecontext")
844
+ ```
845
+
846
+ Without the same context, decryption will fail.
847
+
848
+ ```ruby
849
+ lockbox.decrypt(ciphertext, associated_data: "somecontext") # success
850
+ lockbox.decrypt(ciphertext, associated_data: "othercontext") # fails
851
+ ```
852
+
788
853
  ## Binary Columns
789
854
 
790
- You can use `binary` columns for the ciphertext instead of `text` columns to save space.
855
+ You can use `binary` columns for the ciphertext instead of `text` columns.
791
856
 
792
857
  ```ruby
793
858
  class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
@@ -797,7 +862,7 @@ class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
797
862
  end
798
863
  ```
799
864
 
800
- You should disable Base64 encoding if you do this.
865
+ Disable Base64 encoding to save space.
801
866
 
802
867
  ```ruby
803
868
  class User < ApplicationRecord
@@ -1,7 +1,22 @@
1
- # ideally encrypt and decrypt would happen at the blob/service level
2
- # however, there isn't really a great place to define encryption settings there
3
- # instead, we encrypt and decrypt at the attachment level,
4
- # and we define encryption settings at the model level
1
+ # Ideally encryption and decryption would happen at the blob/service level.
2
+ # However, Active Storage < 6.1 only supports a single service (per environment).
3
+ # This means all attachments need to be encrypted or none of them,
4
+ # which is often not practical.
5
+ #
6
+ # Active Storage 6.1 adds support for multiple services, which changes this.
7
+ # We could have a Lockbox service:
8
+ #
9
+ # lockbox:
10
+ # service: Lockbox
11
+ # backend: local # delegate to another service, like mirror service
12
+ # key: ... # Lockbox options
13
+ #
14
+ # However, the checksum is computed *and stored on the blob*
15
+ # before the file is passed to the service.
16
+ # We don't want the MD5 checksum of the plaintext stored in the database.
17
+ #
18
+ # Instead, we encrypt and decrypt at the attachment level,
19
+ # and we define encryption settings at the model level.
5
20
  module Lockbox
6
21
  module ActiveStorageExtensions
7
22
  module Attached
@@ -95,6 +110,16 @@ module Lockbox
95
110
  result
96
111
  end
97
112
 
113
+ def variant(*args)
114
+ raise Lockbox::Error, "Variant not supported for encrypted files" if Utils.encrypted_options(record, name)
115
+ super
116
+ end
117
+
118
+ def preview(*args)
119
+ raise Lockbox::Error, "Preview not supported for encrypted files" if Utils.encrypted_options(record, name)
120
+ super
121
+ end
122
+
98
123
  if ActiveStorage::VERSION::MAJOR >= 6
99
124
  def open(**options)
100
125
  blob.open(**options) do |file|
@@ -2,9 +2,22 @@ module Lockbox
2
2
  module CarrierWaveExtensions
3
3
  def encrypt(**options)
4
4
  class_eval do
5
+ # uses same hook as process (before cache)
6
+ # processing can be disabled, so better to keep separate
5
7
  before :cache, :encrypt
6
8
 
9
+ define_singleton_method :lockbox_options do
10
+ options
11
+ end
12
+
7
13
  def encrypt(file)
14
+ # safety check
15
+ # see CarrierWave::Uploader::Cache#cache!
16
+ raise Lockbox::Error, "Expected files to be equal. Please report an issue." unless file && @file && file == @file
17
+
18
+ # processors in CarrierWave move updated file to current_path
19
+ # however, this causes versions to use the processed file
20
+ # we only want to change the file for the current version
8
21
  @file = CarrierWave::SanitizedFile.new(lockbox_notify("encrypt_file") { lockbox.encrypt_io(file) })
9
22
  end
10
23
 
@@ -14,6 +27,7 @@ module Lockbox
14
27
  lockbox_notify("decrypt_file") { lockbox.decrypt(r) } if r
15
28
  end
16
29
 
30
+ # use size of plaintext since read and content type use plaintext
17
31
  def size
18
32
  read.bytesize
19
33
  end
@@ -23,6 +37,7 @@ module Lockbox
23
37
  MimeMagic.by_magic(read).try(:type) || "invalid/invalid"
24
38
  end
25
39
 
40
+ # disable processing since already processed
26
41
  def rotate_encryption!
27
42
  io = Lockbox::IO.new(read)
28
43
  io.original_filename = file.filename
@@ -46,6 +61,8 @@ module Lockbox
46
61
  end
47
62
  end
48
63
 
64
+ # for mounted uploaders, use mounted name
65
+ # for others, use uploader name
49
66
  def lockbox_name
50
67
  if mounted_as
51
68
  mounted_as.to_s
@@ -58,6 +75,8 @@ module Lockbox
58
75
  end
59
76
  end
60
77
 
78
+ # Active Support notifications so it's easier
79
+ # to see when files are encrypted and decrypted
61
80
  def lockbox_notify(type)
62
81
  if defined?(ActiveSupport::Notifications)
63
82
  name = lockbox_name
@@ -116,6 +116,13 @@ module Lockbox
116
116
  end
117
117
  end
118
118
 
119
+ # there's a small chance for this process to read data,
120
+ # another process to update the data, and
121
+ # this process to write the now stale data
122
+ # this time window can be reduced with smaller batch sizes
123
+ # locking individual records could eliminate this
124
+ # one option is: relation.in_batches { |batch| batch.lock }
125
+ # which runs SELECT ... FOR UPDATE in Postgres
119
126
  def migrate_records(records, fields:, blind_indexes:, restart:, rotate:)
120
127
  # do computation outside of transaction
121
128
  # especially expensive blind index computation
@@ -87,6 +87,9 @@ module Lockbox
87
87
  # essentially a no-op if already loaded
88
88
  # an exception is thrown if decryption fails
89
89
  self.class.lockbox_attributes.each do |_, lockbox_attribute|
90
+ # don't try to decrypt if no decryption key given
91
+ next if lockbox_attribute[:algorithm] == "hybrid" && lockbox_attribute[:decryption_key].nil?
92
+
90
93
  # it is possible that the encrypted attribute is not loaded, eg.
91
94
  # if the record was fetched partially (`User.select(:id).first`).
92
95
  # accessing a not loaded attribute raises an `ActiveModel::MissingAttributeError`.
@@ -263,12 +266,13 @@ module Lockbox
263
266
  define_method("#{name}=") do |message|
264
267
  # decrypt first for dirty tracking
265
268
  # don't raise error if can't decrypt previous
266
- begin
267
- send(name)
268
- rescue Lockbox::DecryptionError
269
- # this is expected for hybrid cryptography
270
- warn "[lockbox] Decrypting previous value failed" unless options[:algorithm] == "hybrid"
271
- nil
269
+ # don't try to decrypt if no decryption key given
270
+ unless options[:algorithm] == "hybrid" && options[:decryption_key].nil?
271
+ begin
272
+ send(name)
273
+ rescue Lockbox::DecryptionError
274
+ warn "[lockbox] Decrypting previous value failed"
275
+ end
272
276
  end
273
277
 
274
278
  send("lockbox_direct_#{name}=", message)
@@ -16,14 +16,14 @@ module Lockbox
16
16
  end
17
17
 
18
18
  unless options[:key] || options[:encryption_key] || options[:decryption_key]
19
- options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key))
19
+ options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key), encode: false)
20
20
  end
21
21
 
22
22
  if options[:previous_versions].is_a?(Array)
23
23
  options[:previous_versions] = options[:previous_versions].dup
24
24
  options[:previous_versions].each_with_index do |version, i|
25
25
  if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && version[:master_key]
26
- options[:previous_versions][i] = version.merge(key: Lockbox.attribute_key(table: table, attribute: attribute, master_key: version.delete(:master_key)))
26
+ options[:previous_versions][i] = version.merge(key: Lockbox.attribute_key(table: table, attribute: attribute, master_key: version.delete(:master_key), encode: false))
27
27
  end
28
28
  end
29
29
  end
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.4.6"
2
+ VERSION = "0.4.7"
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.4.6
4
+ version: 0.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-03 00:00:00.000000000 Z
11
+ date: 2020-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler