lockbox 0.4.6 → 0.4.7

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