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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +70 -5
- data/lib/lockbox/active_storage_extensions.rb +29 -4
- data/lib/lockbox/carrier_wave_extensions.rb +19 -0
- data/lib/lockbox/migrator.rb +7 -0
- data/lib/lockbox/model.rb +10 -6
- data/lib/lockbox/utils.rb +2 -2
- data/lib/lockbox/version.rb +1 -1
- metadata +2 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 3f8c447dd90537203a1a3038c347c10de0e48f5b29795b382a2a77019e6e5764
         | 
| 4 | 
            +
              data.tar.gz: 9133e9eb0c2132b7c77c39f8c24a3c27ea9b3cbb1d3d82f7f069b2db9992198f
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 4396ee4ead0de0592e7a3574b563f98d58f0402198dd8662cbdc374cc5f8a39e629f6397414b498456c665e8a635518db3a532056ecd42154206fde4ab938e5c
         | 
| 7 | 
            +
              data.tar.gz: 6057ea6f43db261580a0ee0ae13f1303251d2ee8ef2b1bcdba8399b6a858dca1fdf6c84e25f5da85a059a6d260be7094969e2118ff11a8c1d2565617c48f74cd
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    
    
        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 | 
            -
            -  | 
| 268 | 
            -
            -  | 
| 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.  | 
| 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 | 
| 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 | 
            -
             | 
| 865 | 
            +
            Disable Base64 encoding to save space.
         | 
| 801 866 |  | 
| 802 867 | 
             
            ```ruby
         | 
| 803 868 | 
             
            class User < ApplicationRecord
         | 
| @@ -1,7 +1,22 @@ | |
| 1 | 
            -
            #  | 
| 2 | 
            -
            #  | 
| 3 | 
            -
            #  | 
| 4 | 
            -
            #  | 
| 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
         | 
    
        data/lib/lockbox/migrator.rb
    CHANGED
    
    | @@ -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
         | 
    
        data/lib/lockbox/model.rb
    CHANGED
    
    | @@ -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 | 
            -
                         | 
| 267 | 
            -
             | 
| 268 | 
            -
             | 
| 269 | 
            -
             | 
| 270 | 
            -
                           | 
| 271 | 
            -
             | 
| 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)
         | 
    
        data/lib/lockbox/utils.rb
    CHANGED
    
    | @@ -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
         | 
    
        data/lib/lockbox/version.rb
    CHANGED
    
    
    
        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. | 
| 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- | 
| 11 | 
            +
            date: 2020-08-19 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         |