lockbox 1.1.1 → 1.2.0

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: 1a8a7995008cdd49c48d95e0a968e45666276fb095de76954ef949800080d40b
4
- data.tar.gz: da7d7796776c325ce1871a2755f6cc012c060f16293cbe28a31f2547c92ec0ad
3
+ metadata.gz: f26c45499c84a35d3a4aaeb7043853a4090dd63b2268e128fc0f2713dfc25b09
4
+ data.tar.gz: 697626daf2a92ecf69eabc56390074f497ba51a31d1ee24a2a5644c9437d0f09
5
5
  SHA512:
6
- metadata.gz: d97e45d14fbc7f452eef5e09c2b2d8d3b2c2f5d4b8a42645c0138849b21ef61194f7cf2e36158f4e920b7cc5709cef63a9769310f97cdd40c3f78790415100d0
7
- data.tar.gz: f04c5264be1ec1e69c406593bdc143f10f846776a34744d38a368ba0e0d2662c874315b9387c5e7c53a47621cbabb2da146114008d1b5963dcc3e8b845a86ff2
6
+ metadata.gz: 1b0832e473ba43bb36cc9b11398d20385eae5f94b3a2d13f66a5cc4322605f28bf93a22b9fcb9ac5e171e7004b909b59e0f26d5a75d1be6561c61bbdf054c65e
7
+ data.tar.gz: 77995606e64719168bb0688215721e89c84b7ce9b758e49dbbb1beeecd460ca85c129089c8f1f160320ead16cbe64bd7eb2fb72491d7e9575ac392ef11ca492b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 1.2.0 (2023-03-20)
2
+
3
+ - Made it easier to rotate master key
4
+ - Added `associated_data` option for database fields and files
5
+ - Added `decimal` type
6
+ - Added `encode_attributes` option
7
+ - Fixed deprecation warnings with Rails 7.1
8
+
9
+ ## 1.1.2 (2023-02-01)
10
+
11
+ - Fixed error when migrating to `array`, `hash`, and `json` types
12
+
1
13
  ## 1.1.1 (2022-12-08)
2
14
 
3
15
  - Fixed error when `StringIO` not loaded
data/README.md CHANGED
@@ -117,6 +117,7 @@ class User < ApplicationRecord
117
117
  has_encrypted :active, type: :boolean
118
118
  has_encrypted :salary, type: :integer
119
119
  has_encrypted :latitude, type: :float
120
+ has_encrypted :longitude, type: :decimal
120
121
  has_encrypted :video, type: :binary
121
122
  has_encrypted :properties, type: :json
122
123
  has_encrypted :settings, type: :hash
@@ -566,51 +567,25 @@ Use `decrypt_str` get the value as UTF-8
566
567
 
567
568
  To make key rotation easy, you can pass previous versions of keys that can decrypt.
568
569
 
569
- ### Active Record & Mongoid
570
-
571
- Update your model:
570
+ Create `config/initializers/lockbox.rb` with:
572
571
 
573
572
  ```ruby
574
- class User < ApplicationRecord
575
- has_encrypted :email, previous_versions: [{master_key: previous_key}]
576
- end
573
+ Lockbox.default_options[:previous_versions] = [{master_key: previous_key}]
577
574
  ```
578
575
 
579
- To rotate existing records, use:
576
+ To rotate existing Active Record & Mongoid records, use:
580
577
 
581
578
  ```ruby
582
579
  Lockbox.rotate(User, attributes: [:email])
583
580
  ```
584
581
 
585
- Once all records are rotated, you can remove `previous_versions` from the model.
586
-
587
- ### Action Text
588
-
589
- Update your initializer:
590
-
591
- ```ruby
592
- Lockbox.encrypts_action_text_body(previous_versions: [{master_key: previous_key}])
593
- ```
594
-
595
- To rotate existing records, use:
582
+ To rotate existing Action Text records, use:
596
583
 
597
584
  ```ruby
598
585
  Lockbox.rotate(ActionText::RichText, attributes: [:body])
599
586
  ```
600
587
 
601
- Once all records are rotated, you can remove `previous_versions` from the initializer.
602
-
603
- ### Active Storage
604
-
605
- Update your model:
606
-
607
- ```ruby
608
- class User < ApplicationRecord
609
- encrypts_attached :license, previous_versions: [{master_key: previous_key}]
610
- end
611
- ```
612
-
613
- To rotate existing files, use:
588
+ To rotate existing Active Storage files, use:
614
589
 
615
590
  ```ruby
616
591
  User.with_attached_license.find_each do |user|
@@ -618,39 +593,31 @@ User.with_attached_license.find_each do |user|
618
593
  end
619
594
  ```
620
595
 
621
- Once all files are rotated, you can remove `previous_versions` from the model.
622
-
623
- ### CarrierWave
624
-
625
- Update your model:
626
-
627
- ```ruby
628
- class LicenseUploader < CarrierWave::Uploader::Base
629
- encrypt previous_versions: [{master_key: previous_key}]
630
- end
631
- ```
632
-
633
- To rotate existing files, use:
596
+ To rotate existing CarrierWave files, use:
634
597
 
635
598
  ```ruby
636
599
  User.find_each do |user|
637
600
  user.license.rotate_encryption!
601
+ # or for multiple files
602
+ user.licenses.map(&:rotate_encryption!)
638
603
  end
639
604
  ```
640
605
 
641
- For multiple files, use:
606
+ Once everything is rotated, you can remove `previous_versions` from the initializer.
607
+
608
+ ### Individual Fields & Files
609
+
610
+ You can also pass previous versions to individual fields and files.
642
611
 
643
612
  ```ruby
644
- User.find_each do |user|
645
- user.licenses.map(&:rotate_encryption!)
613
+ class User < ApplicationRecord
614
+ has_encrypted :email, previous_versions: [{master_key: previous_key}]
646
615
  end
647
616
  ```
648
617
 
649
- Once all files are rotated, you can remove `previous_versions` from the model.
650
-
651
618
  ### Local Files & Strings
652
619
 
653
- For local files and strings, use:
620
+ To rotate local files and strings, use:
654
621
 
655
622
  ```ruby
656
623
  Lockbox.new(key: key, previous_versions: [{key: previous_key}])
@@ -948,6 +915,12 @@ class User < ApplicationRecord
948
915
  end
949
916
  ```
950
917
 
918
+ or set it globally:
919
+
920
+ ```ruby
921
+ Lockbox.encode_attributes = false
922
+ ```
923
+
951
924
  ## Compatibility
952
925
 
953
926
  It’s easy to read encrypted data in another language if needed.
data/lib/lockbox/box.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  module Lockbox
2
2
  class Box
3
- def initialize(key: nil, algorithm: nil, encryption_key: nil, decryption_key: nil, padding: false)
3
+ NOT_SET = Object.new
4
+
5
+ def initialize(key: nil, algorithm: nil, encryption_key: nil, decryption_key: nil, padding: false, associated_data: nil)
4
6
  raise ArgumentError, "Cannot pass both key and encryption/decryption key" if key && (encryption_key || decryption_key)
5
7
 
6
8
  key = Lockbox::Utils.decode_key(key) if key
@@ -32,9 +34,11 @@ module Lockbox
32
34
 
33
35
  @algorithm = algorithm
34
36
  @padding = padding == true ? 16 : padding
37
+ @associated_data = associated_data
35
38
  end
36
39
 
37
- def encrypt(message, associated_data: nil)
40
+ def encrypt(message, associated_data: NOT_SET)
41
+ associated_data = @associated_data if associated_data == NOT_SET
38
42
  message = Lockbox.pad(message, size: @padding) if @padding
39
43
  case @algorithm
40
44
  when "hybrid"
@@ -53,7 +57,8 @@ module Lockbox
53
57
  nonce + ciphertext
54
58
  end
55
59
 
56
- def decrypt(ciphertext, associated_data: nil)
60
+ def decrypt(ciphertext, associated_data: NOT_SET)
61
+ associated_data = @associated_data if associated_data == NOT_SET
57
62
  message =
58
63
  case @algorithm
59
64
  when "hybrid"
@@ -9,7 +9,7 @@ module Lockbox
9
9
 
10
10
  @boxes =
11
11
  [Box.new(**options)] +
12
- Array(previous_versions).map { |v| Box.new(key: options[:key], **v) }
12
+ Array(previous_versions).reject { |v| v.key?(:master_key) }.map { |v| Box.new(key: options[:key], **v) }
13
13
  end
14
14
 
15
15
  def encrypt(message, **options)
@@ -6,7 +6,7 @@ module Lockbox
6
6
  payload = event.payload
7
7
  name = "Encrypt File (#{event.duration.round(1)}ms)"
8
8
 
9
- debug " #{color(name, YELLOW, true)} Encrypted #{payload[:name]}"
9
+ debug " #{color(name, YELLOW, bold: true)} Encrypted #{payload[:name]}"
10
10
  end
11
11
 
12
12
  def decrypt_file(event)
@@ -15,7 +15,7 @@ module Lockbox
15
15
  payload = event.payload
16
16
  name = "Decrypt File (#{event.duration.round(1)}ms)"
17
17
 
18
- debug " #{color(name, YELLOW, true)} Decrypted #{payload[:name]}"
18
+ debug " #{color(name, YELLOW, bold: true)} Decrypted #{payload[:name]}"
19
19
  end
20
20
  end
21
21
  end
data/lib/lockbox/model.rb CHANGED
@@ -19,10 +19,12 @@ module Lockbox
19
19
  # options[:type] = :integer
20
20
  # when Float
21
21
  # options[:type] = :float
22
+ # when BigDecimal
23
+ # options[:type] = :decimal
22
24
  # end
23
25
 
24
26
  custom_type = options[:type].respond_to?(:serialize) && options[:type].respond_to?(:deserialize)
25
- valid_types = [nil, :string, :boolean, :date, :datetime, :time, :integer, :float, :binary, :json, :hash, :array, :inet]
27
+ valid_types = [nil, :string, :boolean, :date, :datetime, :time, :integer, :float, :decimal, :binary, :json, :hash, :array, :inet]
26
28
  raise ArgumentError, "Unknown type: #{options[:type]}" unless custom_type || valid_types.include?(options[:type])
27
29
 
28
30
  activerecord = defined?(ActiveRecord::Base) && self < ActiveRecord::Base
@@ -50,7 +52,7 @@ module Lockbox
50
52
 
51
53
  options[:attribute] = name.to_s
52
54
  options[:encrypted_attribute] = encrypted_attribute
53
- options[:encode] = true unless options.key?(:encode)
55
+ options[:encode] = Lockbox.encode_attributes unless options.key?(:encode)
54
56
 
55
57
  encrypt_method_name = "generate_#{encrypted_attribute}"
56
58
  decrypt_method_name = "decrypt_#{encrypted_attribute}"
@@ -321,9 +323,15 @@ module Lockbox
321
323
 
322
324
  attribute name, attribute_type
323
325
 
324
- serialize name, JSON if options[:type] == :json
325
- serialize name, Hash if options[:type] == :hash
326
- serialize name, Array if options[:type] == :array
326
+ 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
330
+ 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
334
+ end
327
335
  elsif !attributes_to_define_after_schema_loads.key?(name.to_s)
328
336
  # when migrating it's best to specify the type directly
329
337
  # however, we can try to use the original type if its already defined
@@ -531,6 +539,19 @@ module Lockbox
531
539
  message = ActiveRecord::Type::Float.new.serialize(message)
532
540
  # double precision, big endian
533
541
  message = [message].pack("G") unless message.nil?
542
+ when :decimal
543
+ message =
544
+ if ActiveRecord::VERSION::MAJOR >= 6
545
+ ActiveRecord::Type::Decimal.new.serialize(message)
546
+ else
547
+ # issue with serialize in Active Record < 6
548
+ # https://github.com/rails/rails/commit/a741208f80dd33420a56486bd9ed2b0b9862234a
549
+ ActiveRecord::Type::Decimal.new.cast(message)
550
+ end
551
+ # Postgres stores 4 decimal digits in 2 bytes
552
+ # plus 3 to 8 bytes of overhead
553
+ # but use string for simplicity
554
+ message = message.to_s("F") unless message.nil?
534
555
  when :inet
535
556
  unless message.nil?
536
557
  ip = message.is_a?(IPAddr) ? message : (IPAddr.new(message) rescue nil)
@@ -543,8 +564,8 @@ module Lockbox
543
564
  # do nothing
544
565
  # encrypt will convert to binary
545
566
  else
546
- # use original name for serialized attributes
547
- type = (try(:attribute_types) || {})[original_name.to_s]
567
+ # use original name for serialized attributes if no type specified
568
+ type = (try(:attribute_types) || {})[(options[:type] ? name : original_name).to_s]
548
569
  message = type.serialize(message) if type
549
570
  end
550
571
  end
@@ -576,9 +597,11 @@ module Lockbox
576
597
  when :time
577
598
  message = ActiveRecord::Type::Time.new.deserialize(message)
578
599
  when :integer
579
- message = ActiveRecord::Type::Integer.new(limit: 8).deserialize(message.unpack("q>").first)
600
+ message = ActiveRecord::Type::Integer.new(limit: 8).deserialize(message.unpack1("q>"))
580
601
  when :float
581
- message = ActiveRecord::Type::Float.new.deserialize(message.unpack("G").first)
602
+ message = ActiveRecord::Type::Float.new.deserialize(message.unpack1("G"))
603
+ when :decimal
604
+ message = ActiveRecord::Type::Decimal.new.deserialize(message)
582
605
  when :string
583
606
  message.force_encoding(Encoding::UTF_8)
584
607
  when :binary
@@ -590,8 +613,8 @@ module Lockbox
590
613
  message = IPAddr.new_ntoh(addr.first(len))
591
614
  message.prefix = prefix
592
615
  else
593
- # use original name for serialized attributes
594
- type = (try(:attribute_types) || {})[original_name.to_s]
616
+ # use original name for serialized attributes if no type specified
617
+ type = (try(:attribute_types) || {})[(options[:type] ? name : original_name).to_s]
595
618
  message = type.deserialize(message) if type
596
619
  message.force_encoding(Encoding::UTF_8) if !type || type.is_a?(ActiveModel::Type::String)
597
620
  end
data/lib/lockbox/utils.rb CHANGED
@@ -26,6 +26,10 @@ module Lockbox
26
26
  )
27
27
  end
28
28
 
29
+ unless options.key?(:previous_versions)
30
+ options[:previous_versions] = Lockbox.default_options[:previous_versions]
31
+ end
32
+
29
33
  if options[:previous_versions].is_a?(Array)
30
34
  # dup previous versions array (with map) since elements are updated
31
35
  # dup each version (with dup) since keys are sometimes deleted
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "1.1.1"
2
+ VERSION = "1.2.0"
3
3
  end
data/lib/lockbox.rb CHANGED
@@ -27,10 +27,11 @@ module Lockbox
27
27
  extend Padding
28
28
 
29
29
  class << self
30
- attr_accessor :default_options
30
+ attr_accessor :default_options, :encode_attributes
31
31
  attr_writer :master_key
32
32
  end
33
33
  self.default_options = {}
34
+ self.encode_attributes = true
34
35
 
35
36
  def self.master_key
36
37
  @master_key ||= ENV["LOCKBOX_MASTER_KEY"]
@@ -72,7 +73,7 @@ module Lockbox
72
73
  end
73
74
 
74
75
  def self.to_hex(str)
75
- str.unpack("H*").first
76
+ str.unpack1("H*")
76
77
  end
77
78
 
78
79
  def self.new(**options)
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.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-08 00:00:00.000000000 Z
11
+ date: 2023-03-20 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.3.7
61
+ rubygems_version: 3.4.6
62
62
  signing_key:
63
63
  specification_version: 4
64
64
  summary: Modern encryption for Ruby and Rails