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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +23 -50
- data/lib/lockbox/box.rb +8 -3
- data/lib/lockbox/encryptor.rb +1 -1
- data/lib/lockbox/log_subscriber.rb +2 -2
- data/lib/lockbox/model.rb +34 -11
- data/lib/lockbox/utils.rb +4 -0
- data/lib/lockbox/version.rb +1 -1
- data/lib/lockbox.rb +3 -2
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f26c45499c84a35d3a4aaeb7043853a4090dd63b2268e128fc0f2713dfc25b09
|
4
|
+
data.tar.gz: 697626daf2a92ecf69eabc56390074f497ba51a31d1ee24a2a5644c9437d0f09
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
570
|
-
|
571
|
-
Update your model:
|
570
|
+
Create `config/initializers/lockbox.rb` with:
|
572
571
|
|
573
572
|
```ruby
|
574
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
645
|
-
|
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
|
-
|
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
|
-
|
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:
|
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:
|
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"
|
data/lib/lockbox/encryptor.rb
CHANGED
@@ -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] =
|
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
|
-
|
325
|
-
|
326
|
-
|
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.
|
600
|
+
message = ActiveRecord::Type::Integer.new(limit: 8).deserialize(message.unpack1("q>"))
|
580
601
|
when :float
|
581
|
-
message = ActiveRecord::Type::Float.new.deserialize(message.
|
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
|
data/lib/lockbox/version.rb
CHANGED
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.
|
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.
|
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:
|
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.
|
61
|
+
rubygems_version: 3.4.6
|
62
62
|
signing_key:
|
63
63
|
specification_version: 4
|
64
64
|
summary: Modern encryption for Ruby and Rails
|