rails-panda-sensitive-data 3.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d553ba5052f74136215c8702a875806544e112cdb010d819d485ee0788d10ab5
4
+ data.tar.gz: bc7a5fdb96003b1db4b913a4d1aecff11c85f41f9216612e267d44ab498d364b
5
+ SHA512:
6
+ metadata.gz: d37df001f8e66168694da9b8984f05794acd1fd1e25e051b62db95262463fe3b3e98d846ee40de22844cba199eba1e8991d8d1d3c7a7979fc29cf066f91887a9
7
+ data.tar.gz: 4228dec6ceea4da6fca90014248c1868ac1e2f895f8d9570a383d5321145915c51d40664d21cd6b98aa7d0e8a06368227316a4c3fbc4c86ee0248232e439cd18
data/CHANGELOG.md ADDED
@@ -0,0 +1,108 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [3.0.0] - 2025-11-26
11
+
12
+ ### Added
13
+ - Comprehensive RSpec test suite
14
+ - SimpleCov integration for test coverage tracking
15
+ - `nil_visible_in_db` option to control whether nil values are visible in the database or encrypted
16
+ - `whitespace_visible_in_db` option to control whether whitespace-only strings are visible in the database or encrypted
17
+ - `rails_default` option to `encrypts` to bypass custom logic and use Rails' standard behavior
18
+ - Full dirty tracking support for `has_sensitive_data` attributes:
19
+ - `#{field}_changed?` - Check if field has changed but not saved
20
+ - `#{field}_in_database` - Get value currently in database
21
+ - `#{field}_before_last_save` - Get value before last save
22
+ - `saved_change_to_#{field}?` - Check if field changed in last save
23
+ - `saved_change_to_#{field}` - Get `[before, after]` array of last save change
24
+ - `will_save_change_to_#{field}?` - Check if field will change on next save
25
+ - `#{field}_change_to_be_saved` - Get `[current_db_value, new_value]` array of pending changes
26
+ - Auto-inclusion of module in all ActiveRecord::Base models via `ActiveSupport.on_load`
27
+ - ArgumentError raised when `encrypts` or `has_sensitive_data` are called without attributes
28
+
29
+ ### Changed
30
+ - Module is now automatically included in all ActiveRecord::Base models (no manual inclusion needed)
31
+ - Removed custom `KeyProvider` and `Key` classes - now uses Rails' default key management
32
+ - Renamed `store_nil_as_empty_string` to `nil_visible_in_db` for clarity
33
+ - `empty_string_visible_in_db` now defaults to `true` (was `false` in previous versions)
34
+ - `nil_visible_in_db` now defaults to `true` (nil values stored as nil, not encrypted)
35
+ - Improved code safety and type checking in Encryptor
36
+ - All dirty tracking methods match Rails' standard behavior exactly
37
+ - Improved `attribute_before_last_save` implementation to use Rails' built-in `attribute_before_last_save` method instead of manual method calls
38
+ - Improved `saved_change_to_attribute?` to correctly handle `nil` values (returns `false` when `before_last_save` is `nil` after reload, matching Rails' behavior)
39
+
40
+ ### Fixed
41
+ - Fixed Encryptor to properly pass options to parent class methods
42
+ - Fixed type checking for empty string handling in Encryptor
43
+
44
+ ## [2.0.6] - 2025-10-06
45
+
46
+ ### Changed
47
+ - Fixed loading issues - gem now loads correctly
48
+ - Added frozen string literal comments to all files
49
+
50
+ ### Fixed
51
+ - Fixed module loading and initialization issues
52
+
53
+ ## [2.0.5] - 2025-10-05
54
+
55
+ ### Changed
56
+ - Removed a useless module
57
+
58
+ ## [2.0.4] - 2023-10-10
59
+
60
+ ### Changed
61
+ - Various improvements and bug fixes
62
+
63
+ ## [2.0.2] - 2023-10-08
64
+
65
+ ### Changed
66
+ - Various improvements and bug fixes
67
+
68
+ ## [2.0.1] - 2023-10-06
69
+
70
+ ### Changed
71
+ - Various improvements and bug fixes
72
+
73
+ ## [2.0.0] - 2023-03-18
74
+
75
+ ### Changed
76
+ - Removed `attr_encrypted` dependency
77
+ - Migrated to use ActiveRecord::Encryption (Rails 7.0+)
78
+ - Major refactoring to use Rails' built-in encryption framework
79
+
80
+ ## [1.0.2] - 2023-03-11
81
+
82
+ ### Changed
83
+ - Various improvements and bug fixes
84
+
85
+ ## [1.0.1] - 2022-03-23
86
+
87
+ ### Changed
88
+ - Various improvements and bug fixes
89
+
90
+ ## [1.0.0] - 2018-02-20
91
+
92
+ ### Added
93
+ - Initial release
94
+ - Module to handle encryption for GDPR compliance
95
+ - Support for encrypting sensitive data in ActiveRecord models
96
+ - Support for storing multiple sensitive fields in a single encrypted attribute
97
+
98
+ [Unreleased]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v3.0.0...HEAD
99
+ [3.0.0]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.6...v3.0.0
100
+ [2.0.6]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.5...v2.0.6
101
+ [2.0.5]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.4...v2.0.5
102
+ [2.0.4]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.2...v2.0.4
103
+ [2.0.2]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.1...v2.0.2
104
+ [2.0.1]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.0...v2.0.1
105
+ [2.0.0]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v1.0.2...v2.0.0
106
+ [1.0.2]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v1.0.1...v1.0.2
107
+ [1.0.1]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v1.0.0...v1.0.1
108
+ [1.0.0]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/releases/tag/v1.0.0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 bigbadpanda.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # Rails Panda Sensitive Data
2
+
3
+ A Rails gem that provides enhanced encryption capabilities for handling (and encrypting) sensitive data in ActiveRecord models.
4
+
5
+ ## Features
6
+
7
+ - **Enhanced ActiveRecord encryption** - Extends Rails' built-in `encrypts` method with additional options
8
+ - **Multiple sensitive fields in one attribute** - Store multiple sensitive data fields in a single encrypted attribute using `has_sensitive_data`
9
+ - **Customizable empty string handling** - Control whether empty strings are visible in the database or encrypted
10
+ - **Nil value handling** - Option to control whether nil values are visible in the database or encrypted
11
+ - **Whitespace-only string handling** - Option to store whitespace-only strings as-is (space-saving for non-PII data)
12
+ - **Full dirty tracking support** - Complete ActiveRecord dirty tracking methods for sensitive data fields
13
+ - **Auto-inclusion** - Automatically included in all ActiveRecord::Base models
14
+ - **Rails 7.0+ compatible** - Built on top of ActiveRecord::Encryption
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem "rails-panda-sensitive-data"
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ ```bash
27
+ $ bundle install
28
+ ```
29
+
30
+ ## Requirements
31
+
32
+ - Ruby >= 3.2
33
+ - Rails >= 7.0 (required for ActiveRecord::Encryption support)
34
+ - ActiveRecord::Encryption must be configured in your Rails application
35
+
36
+ ## Configuration
37
+
38
+ ### ActiveRecord::Encryption Setup
39
+
40
+ First, configure ActiveRecord::Encryption in your Rails application:
41
+
42
+ ```ruby
43
+ # config/application.rb
44
+ config.active_record.encryption.primary_key = "your-primary-key-here"
45
+ config.active_record.encryption.deterministic_key = "your-deterministic-key-here"
46
+ config.active_record.encryption.key_derivation_salt = "your-key-derivation-salt-here"
47
+ ```
48
+
49
+ I don't mention including this in `config/initializers/active_record_encryption.rb` because I've often run into problems with this approach. Namely that ActiveRecord was already initialized (read: had fetched these encryption keys) by the time the initializers started running.
50
+
51
+ ## Usage
52
+
53
+ ### Basic Setup
54
+
55
+ The module is automatically included in all ActiveRecord::Base models. No manual inclusion needed!
56
+
57
+ ### Using `encrypts`
58
+
59
+ The `encrypts` method works like Rails' built-in method but with additional options:
60
+
61
+ ```ruby
62
+ class User < ApplicationRecord
63
+ encrypts :email
64
+ encrypts :phone_number
65
+ end
66
+ ```
67
+
68
+ #### Options
69
+
70
+ - `nil_visible_in_db` (default: `true`) - If `true`, nil values are stored as nil in the database (saves space, but visible). If `false`, nil values are encrypted using a sentinel value (`\0`) to distinguish them from encrypted empty strings.
71
+ - `empty_string_visible_in_db` (default: `true`) - If `true`, empty strings are stored as empty strings in the database (saves space, but visible). If `false`, they are encrypted.
72
+ - `whitespace_visible_in_db` (default: `true`) - If `true`, whitespace-only strings (e.g., " ", "\t\n") are stored as-is in the database (saves space, no PII). If `false`, they are encrypted.
73
+ - `rails_default` (default: `false`) - If `true`, bypasses all custom logic and uses Rails' standard `encrypts` behavior.
74
+ - `encryptor` - Custom encryptor instance (if provided, custom encryptor won't be created automatically)
75
+ - All standard Rails `encrypts` options are also supported (e.g., `deterministic`, `key_provider`, etc.)
76
+
77
+ **Example:**
78
+
79
+ ```ruby
80
+ class User < ApplicationRecord
81
+ # Encrypt empty strings instead of storing them as-is
82
+ encrypts :email, empty_string_visible_in_db: false
83
+
84
+ # Encrypt nil values instead of storing them as nil
85
+ encrypts :phone_number, nil_visible_in_db: false
86
+
87
+ # Encrypt whitespace-only strings
88
+ encrypts :notes, whitespace_visible_in_db: false
89
+
90
+ # Use Rails' standard behavior (bypass custom logic)
91
+ encrypts :backup_email, rails_default: true
92
+ end
93
+ ```
94
+
95
+ **Trade-offs:**
96
+
97
+ - **Visible in DB (`true`)**: Saves space, faster queries, but values are visible in the database
98
+ - **Encrypted (`false`)**: More secure, but takes more space. Note: Encrypted empty/nil values can still be inferred from their shorter encrypted blob size.
99
+
100
+ ### Using `has_sensitive_data`
101
+
102
+ Store multiple sensitive data fields in a single encrypted attribute:
103
+
104
+ ```ruby
105
+ class User < ApplicationRecord
106
+ has_sensitive_data :ssn, :credit_card, :bank_account
107
+ end
108
+ ```
109
+
110
+ This will:
111
+ - Store all fields in a single `sensitive_data` encrypted attribute
112
+ - Provide accessor methods for each field (`ssn`, `credit_card`, `bank_account`)
113
+ - Automatically encrypt/decrypt the entire hash
114
+
115
+ **Example:**
116
+
117
+ ```ruby
118
+ user = User.new
119
+ user.ssn = "123-45-6789"
120
+ user.credit_card = "4111-1111-1111-1111"
121
+ user.bank_account = "1234567890"
122
+ user.save!
123
+
124
+ # Reading values
125
+ user.ssn # => "123-45-6789"
126
+ user.credit_card # => "4111-1111-1111-1111"
127
+
128
+ # The sensitive_data attribute is encrypted
129
+ user.read_attribute(:sensitive_data) # => encrypted value, not plain hash
130
+ ```
131
+
132
+ #### Options
133
+
134
+ - `in` (default: `:sensitive_data`) - The attribute name to store the encrypted hash
135
+ - All options from `encrypts` are also supported
136
+
137
+ **Example with custom attribute name:**
138
+
139
+ ```ruby
140
+ class User < ApplicationRecord
141
+ has_sensitive_data :ssn, :credit_card, in: :encrypted_pii
142
+ end
143
+ ```
144
+
145
+ ### Change Tracking
146
+
147
+ The gem provides change tracking methods for sensitive data fields:
148
+
149
+ ```ruby
150
+ user = User.new
151
+ user.ssn = "123-45-6789"
152
+ user.save!
153
+
154
+ user.ssn = "999-99-9999"
155
+ user.ssn_changed? # => true
156
+ user.ssn_in_database # => "123-45-6789"
157
+
158
+ user.save!
159
+ user.saved_change_to_ssn? # => true
160
+ user.ssn_before_last_save # => "123-45-6789"
161
+ ```
162
+
163
+ Available methods (matching Rails' standard dirty tracking API):
164
+ - `#{field}_changed?` - Returns true if the field has been changed but not saved
165
+ - `#{field}_in_database` - Returns the value currently stored in the database
166
+ - `#{field}_before_last_save` - Returns the value before the last save (nil until after a save, and nil after reload)
167
+ - `saved_change_to_#{field}?` - Returns true if the field was changed in the last save
168
+ - `saved_change_to_#{field}` - Returns `[before, after]` array if changed, or `nil`
169
+ - `will_save_change_to_#{field}?` - Returns true if the field will change on the next save
170
+ - `#{field}_change_to_be_saved` - Returns `[current_db_value, new_value]` array if there are pending changes, or `nil`
171
+
172
+ ### Combining `encrypts` and `has_sensitive_data`
173
+
174
+ You can use both methods in the same model:
175
+
176
+ ```ruby
177
+ class User < ApplicationRecord
178
+ encrypts :email
179
+ has_sensitive_data :ssn, :credit_card
180
+ end
181
+ ```
182
+
183
+ ## Advanced Usage
184
+
185
+ ### Custom Encryptor
186
+
187
+ ```ruby
188
+ custom_encryptor = RailsPanda::SensitiveData::Encryption::Encryptor.new(
189
+ empty_string_visible_in_db: false,
190
+ nil_visible_in_db: false,
191
+ whitespace_visible_in_db: false
192
+ )
193
+
194
+ class User < ApplicationRecord
195
+ encrypts :email, encryptor: custom_encryptor
196
+ end
197
+ ```
198
+
199
+ **Note:** If you provide a custom `encryptor`, the gem will not automatically create a custom encryptor based on the visibility options. The provided encryptor will be used as-is.
200
+
201
+ ### Key Management
202
+
203
+ The gem uses Rails' default key provider from your ActiveRecord::Encryption configuration. This supports deterministic encryption and follows Rails' standard key management practices. You can also provide a custom key provider if needed:
204
+
205
+ ```ruby
206
+ # Uses Rails' default key provider (recommended)
207
+ class User < ApplicationRecord
208
+ encrypts :email
209
+ end
210
+
211
+ # Or provide a custom key provider
212
+ custom_key_provider = ActiveRecord::Encryption::KeyProvider.new(primary_key: "your-key")
213
+
214
+ class User < ApplicationRecord
215
+ encrypts :email, key_provider: custom_key_provider
216
+ end
217
+ ```
218
+
219
+ ## How It Works
220
+
221
+ ### `encrypts`
222
+
223
+ This method extends Rails' built-in `encrypts` method with additional options for handling nil values, empty strings, and whitespace-only strings. It uses a custom `Encryptor` that wraps `ActiveRecord::Encryption::Encryptor` and provides space-saving options for non-sensitive values.
224
+
225
+ The custom encryptor is automatically created when visibility options are set to their default values (`true`). You can bypass custom logic entirely by passing `rails_default: true`.
226
+
227
+ ### `has_sensitive_data`
228
+
229
+ This method:
230
+ 1. Serializes multiple fields into a single Hash using YAML
231
+ 2. Encrypts the entire Hash using `encrypts`
232
+ 3. Provides accessor methods for each field
233
+ 4. Handles nil values by removing them from the hash
234
+ 5. Provides full dirty tracking support matching Rails' standard API
235
+
236
+ The encrypted hash is stored in a single database column, reducing the number of encrypted columns needed. All dirty tracking methods work exactly like Rails' built-in methods for regular attributes.
237
+
238
+ ## Development
239
+
240
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
241
+
242
+ ## Contributing
243
+
244
+ Bug reports and pull requests are welcome on GitHub at https://github.com/bigbadpanda-tech/rails-panda-sensitive-data.
245
+
246
+ ## License
247
+
248
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_panda_sensitive_data"
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPanda
4
+ module SensitiveData
5
+ class Config # rubocop:disable Lint/EmptyClass
6
+ end
7
+
8
+ class << self
9
+ def configure
10
+ yield config
11
+ end
12
+
13
+ def config
14
+ @config ||= Config.new
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ # We do not collect coverage for this file because the coverage tool cannot correctly analyze the coverage in this file, due to the way these methods are (re)implemented.
5
+
6
+ module ActiveRecord
7
+ module Encryption
8
+ class EncryptedAttributeType
9
+ alias_method :deserialize_original, :deserialize
10
+ alias_method :serialize_with_current_original, :serialize_with_current
11
+
12
+ # Override deserialize to ensure our custom decryptor converts the sentinel back to nil
13
+ def deserialize(value)
14
+ deserialize_original(value).then do |result|
15
+ # Check special values
16
+ if result == RailsPanda::SensitiveData::Encryption::Encryptor::NIL_SENTINEL
17
+ rails_panda_encryptor&.then do |enc|
18
+ return nil if !enc.instance_variable_get(:@nil_visible_in_db)
19
+ end
20
+ end
21
+ result
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Override serialize_with_current to handle nil values when nil_visible_in_db is false
28
+ # Rails' EncryptedAttributeType checks for nil before calling encrypt, so we need to convert nil to our sentinel value before Rails processes it
29
+ def serialize_with_current(value)
30
+ if value.nil?
31
+ rails_panda_encryptor&.then do |enc|
32
+ return nil if enc.instance_variable_get(:@nil_visible_in_db)
33
+
34
+ # Convert nil to sentinel value and encrypt it directly
35
+ # We need to encrypt it ourselves because Rails' serialize_with_current might store single-byte values directly without encryption
36
+ # Get the key_provider from the scheme or use the default
37
+ key_provider = scheme&.key_provider
38
+ key_provider ||= scheme&.instance_variable_get(:@key_provider_param)
39
+ key_provider ||= ActiveRecord::Encryption.config.then do |config|
40
+ config.is_a?(ActiveRecord::Encryption::KeyProvider) ? config : nil
41
+ end
42
+
43
+ encrypted_sentinel = enc.encrypt(
44
+ RailsPanda::SensitiveData::Encryption::Encryptor::NIL_SENTINEL,
45
+ key_provider: key_provider
46
+ )
47
+ return encrypted_sentinel
48
+ end
49
+ end
50
+
51
+ serialize_with_current_original(value)
52
+ end
53
+
54
+ def rails_panda_encryptor
55
+ scheme&.instance_variable_get(:@context_properties)&.then do |props|
56
+ if props.is_a?(Hash)
57
+ # Get the encryptor from the scheme's context_properties
58
+ enc = props[:encryptor]
59
+ enc.is_a?(RailsPanda::SensitiveData::Encryption::Encryptor) ? enc : nil
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPanda
4
+ module SensitiveData
5
+ module Encryption
6
+ class Encryptor < ::ActiveRecord::Encryption::Encryptor
7
+ # Sentinel value to distinguish encrypted nil from encrypted empty string.
8
+ # When nil_visible_in_db is false, we encrypt nil using this sentinel value (\0) instead of an empty string, allowing us to distinguish between encrypted nil and encrypted empty string during decryption.
9
+ NIL_SENTINEL = "\0"
10
+
11
+ # Custom encryptor that provides control over how nil, empty strings, and whitespace-only strings are stored.
12
+ #
13
+ # @param nil_visible_in_db [Boolean] When true (default), stores nil as nil in the database.
14
+ # This saves space and allows nil values to be queried directly, but makes them visible.
15
+ # When false, encrypts nil using a sentinel value. This hides nil values but takes more space.
16
+ # Note: Encrypted empty values can still be inferred from their shorter encrypted blob size.
17
+ #
18
+ # @param empty_string_visible_in_db [Boolean] When true (default), stores empty strings as ""
19
+ # in the database. This saves space and allows empty strings to be queried directly, but makes them visible. When false, encrypts empty strings normally. This hides empty strings but takes more space. Note: Encrypted empty values can still be inferred from their shorter encrypted blob size.
20
+ #
21
+ # @param whitespace_visible_in_db [Boolean] When true (default), stores whitespace-only strings (e.g., " ", "\t\n") as-is in the database. This saves space since whitespace-only strings contain no PII or sensitive data. When false, encrypts whitespace-only strings normally.
22
+ #
23
+ # Trade-offs:
24
+ # - true: Saves space, faster queries, but values are visible in the database
25
+ # - false: Encrypts values (more secure), but takes more space and encrypted blobs can be inferred as empty/nil from their size
26
+ def initialize(
27
+ nil_visible_in_db: true,
28
+ empty_string_visible_in_db: true,
29
+ whitespace_visible_in_db: true
30
+ )
31
+ super()
32
+
33
+ @nil_visible_in_db = nil_visible_in_db
34
+ @empty_string_visible_in_db = empty_string_visible_in_db
35
+ @whitespace_visible_in_db = whitespace_visible_in_db
36
+ end
37
+
38
+ def encrypt(clear_text, **options)
39
+ if clear_text.nil?
40
+ if @nil_visible_in_db
41
+ nil
42
+ else
43
+ # When nil_visible_in_db is false, encrypt nil using a sentinel value
44
+ # This allows us to distinguish encrypted nil from encrypted empty string
45
+ # Rails' encryptor only accepts strings, so we use a null byte as sentinel
46
+ super(NIL_SENTINEL, **options)
47
+ end
48
+ elsif clear_text.is_a?(String)
49
+ if @empty_string_visible_in_db && clear_text == ""
50
+ ""
51
+ elsif @whitespace_visible_in_db && whitespace_only?(clear_text)
52
+ clear_text
53
+ else
54
+ super
55
+ end
56
+ else
57
+ super
58
+ end
59
+ end
60
+
61
+ def decrypt(encrypted_text, **options)
62
+ if @nil_visible_in_db && encrypted_text.nil?
63
+ nil
64
+ elsif @empty_string_visible_in_db && encrypted_text.is_a?(String) && encrypted_text == ""
65
+ ""
66
+ elsif @whitespace_visible_in_db && encrypted_text.is_a?(String) && whitespace_only?(encrypted_text)
67
+ encrypted_text
68
+ else
69
+ decrypted = super
70
+ # If decrypted value is the nil sentinel, return nil instead
71
+ if decrypted == NIL_SENTINEL && !@nil_visible_in_db
72
+ nil
73
+ else
74
+ decrypted
75
+ end
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ # Check if a string contains only whitespace characters (spaces, tabs, newlines, etc.) but is not empty (empty strings are handled separately)
82
+ def whitespace_only?(str)
83
+ !str.empty? && str.strip.empty?
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/concern"
5
+
6
+ module RailsPanda
7
+ module SensitiveData
8
+ module Models
9
+ module ActiveRecord
10
+ extend ActiveSupport::Concern
11
+
12
+ class_methods do
13
+ def encrypts(*attributes, **options)
14
+ raise ArgumentError, "encrypts must be called with at least one attribute" if attributes.blank?
15
+
16
+ super_options = options.dup
17
+
18
+ # If rails_default is true, skip our custom logic and use Rails' standard behavior
19
+ unless super_options.delete(:rails_default)
20
+ nil_visible_in_db = super_options.delete(:nil_visible_in_db) != false
21
+ empty_string_visible_in_db = super_options.delete(:empty_string_visible_in_db) != false
22
+ whitespace_visible_in_db = super_options.delete(:whitespace_visible_in_db) != false
23
+ needs_custom_encryptor =
24
+ nil_visible_in_db ||
25
+ empty_string_visible_in_db ||
26
+ whitespace_visible_in_db
27
+
28
+ # Pass encryptor as a keyword argument - Rails stores it in context_properties internally and retrieves it from there when needed
29
+ if needs_custom_encryptor && !super_options[:encryptor]
30
+ super_options[:encryptor] =
31
+ ::RailsPanda::SensitiveData::Encryption::Encryptor.new(
32
+ empty_string_visible_in_db:,
33
+ nil_visible_in_db:,
34
+ whitespace_visible_in_db:
35
+ )
36
+ end
37
+ end
38
+
39
+ super(*attributes, **super_options)
40
+ end
41
+
42
+ def has_sensitive_data(*attributes, **options)
43
+ raise ArgumentError, "has_sensitive_data must be called with at least one attribute" if attributes.blank?
44
+
45
+ super_options = options.dup
46
+
47
+ in_attribute = super_options.delete(:in)&.to_sym || :sensitive_data
48
+
49
+ unless @__has_registered_sensitive_data_encrypted_attribute
50
+ serialize(in_attribute, type: Hash, coder: YAML)
51
+ encrypts(in_attribute, **super_options)
52
+
53
+ @__has_registered_sensitive_data_encrypted_attribute = true
54
+ end
55
+
56
+ attributes.each do |attr|
57
+ attr = attr.to_sym
58
+
59
+ define_method attr do
60
+ the_hash = send(in_attribute)
61
+ return if the_hash.blank?
62
+ the_hash[attr]
63
+ end
64
+
65
+ define_method "#{attr}=" do |the_value|
66
+ the_hash = send(in_attribute)
67
+ the_hash ||= {}
68
+
69
+ if the_value.nil?
70
+ the_hash.delete attr
71
+ else
72
+ the_hash[attr] = the_value
73
+ end
74
+
75
+ send("#{in_attribute}=", the_hash)
76
+ end
77
+
78
+ attr_in_database = :"#{attr}_in_database"
79
+ attr_before_last_save = :"#{attr}_before_last_save"
80
+ saved_change_to_attr = :"saved_change_to_#{attr}"
81
+
82
+ define_method attr_in_database do
83
+ attribute_in_database(in_attribute)&.dig(attr)
84
+ end
85
+
86
+ # Returns nil until after a save, and nil after reload (Rails clears mutations_before_last_save on reload)
87
+ define_method attr_before_last_save do
88
+ attribute_before_last_save(in_attribute)&.dig(attr)
89
+ end
90
+
91
+ # attribute_changed? - Has this attribute changed from the database value?
92
+ define_method "#{attr}_changed?" do
93
+ send(attr) != send(attr_in_database)
94
+ end
95
+
96
+ # saved_change_to_attribute - Returns the change to an attribute during the last save
97
+ define_method saved_change_to_attr do
98
+ before = send(attr_before_last_save)
99
+ current = send(attr)
100
+ return nil if before.nil? || before == current
101
+ [before, current]
102
+ end
103
+
104
+ # saved_change_to_attribute? - Did this attribute change when we last saved?
105
+ # Returns false if before_last_save is nil (e.g., after reload or before any save)
106
+ define_method "#{saved_change_to_attr}?" do
107
+ before = send(attr_before_last_save)
108
+ !before.nil? && send(attr) != before # No saved change if before_last_save is nil
109
+ end
110
+
111
+ # will_save_change_to_attribute? - Will this attribute change the next time we save?
112
+ define_method "will_save_change_to_#{attr}?" do
113
+ send(attr) != send(attr_in_database)
114
+ end
115
+
116
+ # attribute_change_to_be_saved - Returns the change to an attribute that will be persisted during the next save
117
+ define_method "#{attr}_change_to_be_saved" do
118
+ in_db = send(attr_in_database)
119
+ current = send(attr)
120
+ return nil if in_db.nil? || in_db == current
121
+ [in_db, current]
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ # Auto-include the module in all ActiveRecord::Base models
132
+ ActiveSupport.on_load(:active_record) do
133
+ include RailsPanda::SensitiveData::Models::ActiveRecord
134
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/concern"
5
+
6
+ module RailsPanda
7
+ module SensitiveData
8
+ module Models
9
+ module Mongoid
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ extend ::AttrEncrypted
14
+
15
+ before_save :set_encryption_key
16
+
17
+ field :encryption_key, type: String
18
+ end
19
+
20
+ def set_encryption_key
21
+ encr_key = encryption_key
22
+ if encr_key.nil? || encr_key == ""
23
+ # 8 chars
24
+ self.encryption_key = SecureRandom.hex(4)
25
+ end
26
+ end
27
+
28
+ def sensitive_data_encryption_key
29
+ set_encryption_key
30
+ encryption_key + ::RailsPanda::SensitiveData.application_sensitive_data_encryption_key
31
+ end
32
+
33
+ class_methods do
34
+ def add_encrypted_attribute(attribute_name, opts = {})
35
+ treat_nil_as_empty_value =
36
+ opts.delete(:treat_nil_as_empty_value) ? true : false
37
+
38
+ empty_value_visible_in_db =
39
+ opts.delete(:empty_value_visible_in_db) ? true : false
40
+
41
+ nil_value_visible_in_db =
42
+ (opts.delete(:nil_value_visible_in_db) ? true : false) ||
43
+ (treat_nil_as_empty_value && empty_value_visible_in_db)
44
+
45
+ prefix = opts[:prefix] || "encrypted_"
46
+ suffix = opts[:suffix] || ""
47
+
48
+ db_attribute_name = [prefix, attribute_name, suffix].join
49
+
50
+ attribute_name = opts.delete(:as) || attribute_name
51
+
52
+ field_name =
53
+ opts[:attribute] ||
54
+ [prefix, attribute_name, suffix].join
55
+
56
+ field db_attribute_name, as: field_name, type: String
57
+ field "#{db_attribute_name}_iv", as: "#{field_name}_iv", type: String
58
+
59
+ attr_encryptor(
60
+ attribute_name,
61
+ key: :sensitive_data_encryption_key,
62
+ marshal: true,
63
+ encode: true,
64
+ allow_empty_value: true,
65
+ **opts
66
+ )
67
+
68
+ encryptor_getter_method_name = "__encryptor_#{attribute_name}"
69
+ encryptor_setter_method_name = "__encryptor_#{attribute_name}="
70
+ mem_value_attr = "@#{attribute_name}"
71
+
72
+ attribute_name_before_last_save = :"#{attribute_name}_before_last_save"
73
+
74
+ alias_method encryptor_getter_method_name, attribute_name
75
+ alias_method encryptor_setter_method_name, "#{attribute_name}="
76
+
77
+ attr_reader attribute_name_before_last_save
78
+ after_initialize do
79
+ if respond_to? encrypted_attribute_name
80
+ instance_variable_set(
81
+ "@#{attribute_name_before_last_save}",
82
+ Marshal.load(Marshal.dump(send(attribute_name)))
83
+ )
84
+ end
85
+ end
86
+
87
+ define_method "saved_change_to_#{attribute_name}?" do
88
+ send(attribute_name) != send(attribute_name_before_last_save)
89
+ end
90
+
91
+ define_method attribute_name do
92
+ unless (value = instance_variable_get(mem_value_attr))
93
+
94
+ db_value = send(encrypted_attribute_name)
95
+
96
+ if nil_value_visible_in_db && db_value.nil?
97
+ return "" if treat_nil_as_empty_value
98
+ return nil
99
+ end
100
+
101
+ if empty_value_visible_in_db && db_value == ""
102
+ return ""
103
+ end
104
+
105
+ value = send(encryptor_getter_method_name)
106
+ end
107
+
108
+ return "" if treat_nil_as_empty_value && value.nil?
109
+ value
110
+ end
111
+
112
+ define_method "#{attribute_name}=" do |the_value|
113
+ encrypted_attribute_name__set = "#{field_name}="
114
+ encrypted_attribute_iv_name__set = "#{field_name}_iv="
115
+
116
+ if nil_value_visible_in_db && the_value.nil? && !treat_nil_as_empty_value
117
+ send(encrypted_attribute_name__set, nil)
118
+ send(encrypted_attribute_iv_name__set, nil)
119
+ instance_variable_set(mem_value_attr, nil)
120
+ return nil
121
+ end
122
+
123
+ if empty_value_visible_in_db &&
124
+ (
125
+ the_value == "" ||
126
+ (treat_nil_as_empty_value && the_value.nil?)
127
+ )
128
+ send(encrypted_attribute_name__set, "")
129
+ send(encrypted_attribute_iv_name__set, "")
130
+ instance_variable_set(mem_value_attr, "")
131
+ return ""
132
+ end
133
+
134
+ the_value = "" if treat_nil_as_empty_value && the_value.nil?
135
+
136
+ send(encryptor_setter_method_name, the_value)
137
+ end
138
+ end
139
+
140
+ def add_sensitive_attribute_accessors(attribute_name, opts = {})
141
+ in_attribute = opts.delete(:in) || :sensitive_data
142
+ in_attribute_before_last_save = :"#{in_attribute}_before_last_save"
143
+ in_field_name =
144
+ opts[:attribute] ||
145
+ opts.delete(:in_db) ||
146
+ [prefix, in_attribute, suffix].join
147
+
148
+ mem_value_attr = "@#{in_attribute}"
149
+
150
+ unless respond_to? in_attribute
151
+ field in_field_name, type: String
152
+ field "#{in_field_name}_iv", type: String
153
+
154
+ attr_encryptor(
155
+ in_attribute,
156
+ key: :sensitive_data_encryption_key,
157
+ marshal: true,
158
+ encode: true,
159
+ allow_empty_value: true,
160
+ attribute: in_field_name,
161
+ **opts
162
+ )
163
+
164
+ attr_reader in_attribute_before_last_save
165
+ after_initialize do
166
+ if respond_to?(in_field_name)
167
+ instance_variable_set(
168
+ "@#{in_attribute_before_last_save}",
169
+ Marshal.load(Marshal.dump(send(in_attribute)))
170
+ )
171
+ end
172
+ end
173
+ end
174
+
175
+ define_method "saved_change_to_#{attribute_name}?" do
176
+ send(attribute_name) != send(attribute_name_before_last_save)
177
+ end
178
+
179
+ define_method "#{attribute_name}_before_last_save" do
180
+ send(in_attribute_before_last_save)&.dig(attribute_name)
181
+ end
182
+
183
+ define_method attribute_name do
184
+ unless (the_hash = instance_variable_get(mem_value_attr))
185
+ db_value = send(in_field_name)
186
+ return nil if db_value.nil? || db_value == ""
187
+
188
+ the_hash = send(in_attribute)
189
+ end
190
+ return nil if the_hash.nil? || the_hash == ""
191
+
192
+ the_hash.dig(attribute_name)
193
+ end
194
+
195
+ define_method "#{attribute_name}=" do |the_value|
196
+ unless (the_hash = instance_variable_get(mem_value_attr))
197
+ db_value = send(in_field_name)
198
+ unless db_value.nil? || db_value == ""
199
+ the_hash = send(in_attribute)
200
+ end
201
+ end
202
+ the_hash = {} if the_hash.nil? || the_hash == ""
203
+
204
+ if the_value.nil?
205
+ the_hash.delete attribute_name
206
+ else
207
+ the_hash[attribute_name] = the_value
208
+ end
209
+
210
+ send("#{in_attribute}=", the_hash)
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_panda_sensitive_data/config"
4
+
5
+ module RailsPanda
6
+ module SensitiveData
7
+ class << self # rubocop:disable Lint/EmptyClass
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPanda
4
+ module SensitiveData
5
+ VERSION = "3.0.0"
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_panda_sensitive_data/sensitive_data"
4
+ require "rails_panda_sensitive_data/encryption/encryptor"
5
+ require "rails_panda_sensitive_data/encryption/active_record__encrypted_attribute_type"
6
+ require "rails_panda_sensitive_data/models/active_record"
7
+ # require "rails_panda_sensitive_data/models/mongoid"
8
+
9
+ module RailsPanda
10
+ module SensitiveData
11
+ end
12
+ end
@@ -0,0 +1,44 @@
1
+ $LOAD_PATH.push File.expand_path("lib", __dir__)
2
+
3
+ require "rails_panda_sensitive_data/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.required_ruby_version = ">= 3.2"
7
+
8
+ spec.name = "rails-panda-sensitive-data"
9
+ spec.version = RailsPanda::SensitiveData::VERSION
10
+ spec.authors = ["João Saraiva"]
11
+ spec.email = ["panda@bigbadpanda.com"]
12
+ spec.homepage = "https://github.com/bigbadpanda-tech/rails-panda-sensitive-data"
13
+ spec.summary = "Code that Rails applications use for dealing with sensitive data (e.g., GDPR)."
14
+ spec.description = "Code that Rails applications use for dealing with sensitive data (e.g., GDPR)."
15
+ spec.license = "MIT"
16
+ spec.metadata["rubygems_mfa_required"] = "true"
17
+
18
+ spec.files = Dir[
19
+ "lib/**/*",
20
+ "rails_panda_sensitive_data.gemspec",
21
+ "Gemfile",
22
+ # "Rakefile",
23
+ "LICENSE",
24
+ "CHANGELOG.md",
25
+ "README.md"
26
+ ]
27
+
28
+ spec.add_dependency "rails", ">= 7.0.0"
29
+
30
+ # spec.add_development_dependency "combustion"
31
+ spec.add_development_dependency "rake"
32
+ spec.add_development_dependency "rspec"
33
+ spec.add_development_dependency "rspec-rails"
34
+ spec.add_development_dependency "simplecov"
35
+ spec.add_development_dependency "rubocop"
36
+ spec.add_development_dependency "rubocop-rails"
37
+ spec.add_development_dependency "rubocop-rspec"
38
+ spec.add_development_dependency "rubocop-rspec_rails"
39
+ spec.add_development_dependency "rubocop-rake"
40
+ spec.add_development_dependency "rubocop-performance"
41
+ spec.add_development_dependency "standard"
42
+ spec.add_development_dependency "standard-rails"
43
+ spec.add_development_dependency "sqlite3"
44
+ end
metadata ADDED
@@ -0,0 +1,252 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-panda-sensitive-data
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.0.0
5
+ platform: ruby
6
+ authors:
7
+ - João Saraiva
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 7.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 7.0.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec-rails
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: simplecov
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rubocop-rails
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rubocop-rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rubocop-rspec_rails
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rubocop-rake
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: rubocop-performance
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ - !ruby/object:Gem::Dependency
167
+ name: standard
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ - !ruby/object:Gem::Dependency
181
+ name: standard-rails
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ type: :development
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ - !ruby/object:Gem::Dependency
195
+ name: sqlite3
196
+ requirement: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ type: :development
202
+ prerelease: false
203
+ version_requirements: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0'
208
+ description: Code that Rails applications use for dealing with sensitive data (e.g.,
209
+ GDPR).
210
+ email:
211
+ - panda@bigbadpanda.com
212
+ executables: []
213
+ extensions: []
214
+ extra_rdoc_files: []
215
+ files:
216
+ - CHANGELOG.md
217
+ - Gemfile
218
+ - LICENSE
219
+ - README.md
220
+ - lib/rails/panda/sensitive/data.rb
221
+ - lib/rails_panda_sensitive_data.rb
222
+ - lib/rails_panda_sensitive_data/config.rb
223
+ - lib/rails_panda_sensitive_data/encryption/active_record__encrypted_attribute_type.rb
224
+ - lib/rails_panda_sensitive_data/encryption/encryptor.rb
225
+ - lib/rails_panda_sensitive_data/models/active_record.rb
226
+ - lib/rails_panda_sensitive_data/models/mongoid.rb
227
+ - lib/rails_panda_sensitive_data/sensitive_data.rb
228
+ - lib/rails_panda_sensitive_data/version.rb
229
+ - rails_panda_sensitive_data.gemspec
230
+ homepage: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data
231
+ licenses:
232
+ - MIT
233
+ metadata:
234
+ rubygems_mfa_required: 'true'
235
+ rdoc_options: []
236
+ require_paths:
237
+ - lib
238
+ required_ruby_version: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '3.2'
243
+ required_rubygems_version: !ruby/object:Gem::Requirement
244
+ requirements:
245
+ - - ">="
246
+ - !ruby/object:Gem::Version
247
+ version: '0'
248
+ requirements: []
249
+ rubygems_version: 3.7.2
250
+ specification_version: 4
251
+ summary: Code that Rails applications use for dealing with sensitive data (e.g., GDPR).
252
+ test_files: []