ff1 1.2.0 → 1.2.3

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: c42e1200976768d8adee07820e93d0281a2487f684df7c10ec5f0be591a28716
4
- data.tar.gz: c76fd86787b72671b1fb6de320f5243ba4878471356801f32d568d5f34bc159c
3
+ metadata.gz: 0b5010b97391cf4b071b1b1d8456a3e1070bc38c885fa1f7d2376955bdfd0afd
4
+ data.tar.gz: 394daa33fe6d6238c6bd7e183e81d997ac729c02a91dd0bb1321414922b73e86
5
5
  SHA512:
6
- metadata.gz: 6747aa29273bd6667ec62d5774b1627c6c11a68398c7bbaa4f744867fd66ce14ffa893ac670d970c23269db221d8236b82dcd570b8f1befd0e9dc4324f812b9b
7
- data.tar.gz: e0e820f43cf4bcd79e16fbfded42f7faef5368698b385a2c493e17ca87efbf9801ac23ce545a0757a09b3a803393cdf9739b6f9f83342b9502730790c0dec032
6
+ metadata.gz: '019f223606c49369c2bdf48a1b8d5fd3a32da0e097cb76aa0e03690b4f83ed60f5e744ba4e73f25674d290316cbad6c3b52fe08379c14493e0391a2e494764d1'
7
+ data.tar.gz: e8c2b73a05050f5c33d8801320c58ab98e26863dd2c6767d173187313c7f71063a384cb83b6afa262f4e33f3a4b16520a0feb6deae1adfc6eb2091de101a16ca
data/README.md CHANGED
@@ -14,8 +14,12 @@ The FF1 algorithm is one of two methods specified in NIST Special Publication 80
14
14
  * **Dual-mode operation**: Reversible and Irreversible encryption
15
15
  * Support for any radix from 2 to 65, 536
16
16
  * **NEW: Full UTF-8 text encryption support** - encrypt arbitrary text while maintaining FF1 security properties
17
+ * **NEW: Rails/ActiveRecord integration** - seamless database encryption with GDPR compliance
17
18
  * Tweak support for additional security
18
- * GDPR "right to be forgotten" compliance
19
+ * GDPR "right to be forgotten" compliance with soft delete
20
+ * Automatic encryption/decryption in Rails models
21
+ * Rails migration generators and configuration
22
+ * Query scopes for active/deleted records
19
23
  * Proper input validation and error handling
20
24
  * Comprehensive test suite
21
25
  * Thread-safe implementation
@@ -116,6 +120,215 @@ irreversibly_encrypted = irreversible_cipher.encrypt_text(sensitive_data)
116
120
  # Cannot decrypt - data is permanently transformed while maintaining consistency
117
121
  ```
118
122
 
123
+ ## ActiveRecord Integration (NEW!)
124
+
125
+ The gem now includes full Rails/ActiveRecord integration for seamless database encryption with GDPR compliance features.
126
+
127
+ ### Installation for Rails
128
+
129
+ Add the gem to your Gemfile:
130
+
131
+ ```ruby
132
+ gem 'ff1'
133
+ ```
134
+
135
+ Run the generator to create the configuration file:
136
+
137
+ ```bash
138
+ rails generate ff1:install
139
+ ```
140
+
141
+ This creates `config/initializers/ff1.rb` with configuration options.
142
+
143
+ ### Configuration
144
+
145
+ Configure FF1 in your Rails initializer:
146
+
147
+ ```ruby
148
+ # config/initializers/ff1.rb
149
+ FF1::ActiveRecord.configure do |config|
150
+ # Required: Set your encryption key (store securely!)
151
+ config.global_key = Rails.application.credentials.ff1_encryption_key
152
+ # Or use environment variable:
153
+ # config.global_key = ENV['FF1_ENCRYPTION_KEY']&.b
154
+
155
+ # Optional: Set default encryption mode
156
+ config.default_mode = FF1::Modes::REVERSIBLE
157
+
158
+ # Optional: Customize column names for soft delete
159
+ config.deleted_at_column = :deleted_at
160
+ config.ff1_deleted_column = :ff1_deleted
161
+ end
162
+ ```
163
+
164
+ ### Basic Model Integration
165
+
166
+ Include `FF1::ActiveRecord` in your models and configure encrypted columns:
167
+
168
+ ```ruby
169
+ class User < ApplicationRecord
170
+ include FF1::ActiveRecord
171
+
172
+ # Encrypt email and phone with reversible encryption (can decrypt)
173
+ ff1_encrypt :email, :phone, mode: :reversible
174
+
175
+ # Encrypt SSN with irreversible encryption (cannot decrypt - GDPR compliant)
176
+ ff1_encrypt :ssn, mode: :irreversible
177
+ end
178
+ ```
179
+
180
+ ### Database Migration
181
+
182
+ Generate and run migration to add soft delete columns:
183
+
184
+ ```bash
185
+ rails generate ff1:install User
186
+ rails db:migrate
187
+ ```
188
+
189
+ This adds `deleted_at` and `ff1_deleted` columns to your users table.
190
+
191
+ ### Usage Examples
192
+
193
+ ```ruby
194
+ # Create user - data is automatically encrypted before saving
195
+ user = User.create!(
196
+ name: 'John Doe',
197
+ email: 'john@example.com',
198
+ phone: '555-1234',
199
+ ssn: '123-45-6789'
200
+ )
201
+
202
+ # Access data - reversible columns decrypt automatically
203
+ puts user.email # => 'john@example.com' (decrypted)
204
+ puts user.phone # => '555-1234' (decrypted)
205
+ puts user.ssn # => '[ENCRYPTED]' (irreversible, cannot decrypt)
206
+
207
+ # Data is encrypted in the database
208
+ User.connection.select_value("SELECT email FROM users WHERE id = #{user.id}")
209
+ # => "8224999410799188" (encrypted format)
210
+ ```
211
+
212
+ ### GDPR Compliant Soft Delete
213
+
214
+ The integration provides GDPR-compliant "right to be forgotten" functionality:
215
+
216
+ ```ruby
217
+ # Soft delete with irreversible encryption
218
+ user.destroy # Encrypts all sensitive data irreversibly, keeps record
219
+
220
+ # Check deletion status
221
+ user.ff1_deleted? # => true
222
+ user.ff1_deleted_at # => 2023-01-01 12:00:00 UTC
223
+
224
+ # Data is now irreversibly encrypted
225
+ user.email # => '[ENCRYPTED]'
226
+ user.phone # => '[ENCRYPTED]'
227
+ user.ssn # => '[ENCRYPTED]'
228
+
229
+ # Restore record (but data remains encrypted)
230
+ user.ff1_restore
231
+ user.ff1_deleted? # => false
232
+
233
+ # True hard delete if needed
234
+ user.ff1_hard_destroy!
235
+ ```
236
+
237
+ ### Query Scopes
238
+
239
+ Use built-in scopes to query active vs soft-deleted records:
240
+
241
+ ```ruby
242
+ # Active (non-deleted) users only
243
+ User.ff1_active.count # => 150
244
+ User.ff1_active.where(...) # Chain with other scopes
245
+
246
+ # Soft-deleted users only
247
+ User.ff1_deleted.count # => 25
248
+
249
+ # All users (active + deleted)
250
+ User.ff1_all.count # => 175
251
+
252
+ # Recently deleted users
253
+ User.ff1_recently_deleted(within: 30.days)
254
+
255
+ # Old deleted users (for purging)
256
+ User.ff1_old_deleted(older_than: 1.year)
257
+ ```
258
+
259
+ ### Bulk Operations
260
+
261
+ ```ruby
262
+ # Bulk soft delete with encryption
263
+ User.ff1_destroy_all(status: 'inactive') # Returns count of deleted records
264
+
265
+ # Purge old soft-deleted records
266
+ User.ff1_purge_deleted(older_than: 2.years) # Permanent deletion
267
+
268
+ # Batch encrypt existing data
269
+ User.ff1_encrypt_existing_data!(columns: [:email, :phone])
270
+
271
+ # Statistics
272
+ User.ff1_stats
273
+ # => {
274
+ # total_records: 175,
275
+ # active_records: 150,
276
+ # deleted_records: 25,
277
+ # encrypted_columns: [:email, :phone, :ssn],
278
+ # deletion_rate: 14.29
279
+ # }
280
+ ```
281
+
282
+ ### Advanced Configuration
283
+
284
+ ```ruby
285
+ class User < ApplicationRecord
286
+ include FF1::ActiveRecord
287
+
288
+ # Per-column configuration
289
+ ff1_encrypt :email,
290
+ mode: :reversible,
291
+ key: Rails.application.credentials.user_email_key, # Custom key
292
+ radix: 256 # For text data
293
+
294
+ ff1_encrypt :credit_card_number,
295
+ mode: :reversible,
296
+ radix: 10 # For numeric data
297
+
298
+ ff1_encrypt :sensitive_notes,
299
+ mode: :irreversible, # Cannot be decrypted
300
+ radix: 256
301
+ end
302
+ ```
303
+
304
+ ### Thread Safety
305
+
306
+ The ActiveRecord integration is thread-safe and caches cipher instances for performance:
307
+
308
+ ```ruby
309
+ # Safe for concurrent access
310
+ User.transaction do
311
+ User.create!(email: 'user1@example.com')
312
+ User.create!(email: 'user2@example.com')
313
+ end
314
+ ```
315
+
316
+ ### Performance Considerations
317
+
318
+ 1. **Cipher Caching**: Ciphers are cached per column configuration for performance
319
+ 2. **Bulk Operations**: Use `ff1_find_each` for processing large datasets
320
+ 3. **Indexing**: Encrypted data cannot be indexed for searching - plan accordingly
321
+ 4. **Query Performance**: Use the provided scopes which include proper indexes
322
+
323
+ ### GDPR Compliance Features
324
+
325
+ The ActiveRecord integration specifically supports GDPR requirements:
326
+
327
+ 1. **Right to be Forgotten**: `destroy` method irreversibly encrypts sensitive data
328
+ 2. **Data Minimization**: Only configured columns are encrypted
329
+ 3. **Audit Trail**: Soft delete maintains record for compliance tracking
330
+ 4. **Data Portability**: Reversible encryption allows data export when legally required
331
+
119
332
  ### Key Requirements
120
333
 
121
334
  The FF1 algorithm supports AES key lengths:
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example of FF1 ActiveRecord integration usage
4
+ # This example demonstrates how to use the FF1 gem with Rails/ActiveRecord
5
+ # for database encryption with GDPR compliance features.
6
+
7
+ require 'bundler/setup'
8
+ require 'active_record'
9
+ require 'sqlite3'
10
+ require_relative '../lib/ff1'
11
+
12
+ # Configure ActiveRecord with in-memory SQLite database
13
+ ActiveRecord::Base.establish_connection(
14
+ adapter: 'sqlite3',
15
+ database: ':memory:'
16
+ )
17
+
18
+ # Create test schema
19
+ ActiveRecord::Schema.define do
20
+ create_table :users do |t|
21
+ t.string :name
22
+ t.string :email
23
+ t.string :phone
24
+ t.string :ssn
25
+ t.datetime :deleted_at
26
+ t.boolean :ff1_deleted, default: false
27
+ t.timestamps
28
+ end
29
+
30
+ create_table :posts do |t|
31
+ t.string :title
32
+ t.text :content
33
+ t.integer :user_id
34
+ t.datetime :deleted_at
35
+ t.boolean :ff1_deleted, default: false
36
+ t.timestamps
37
+ end
38
+ end
39
+
40
+ # Configure FF1
41
+ FF1::ActiveRecord.configure do |config|
42
+ config.global_key = SecureRandom.bytes(32)
43
+ config.default_mode = FF1::Modes::REVERSIBLE
44
+ end
45
+
46
+ # Define models with FF1 encryption
47
+ class User < ActiveRecord::Base
48
+ include FF1::ActiveRecord
49
+
50
+ # Encrypt email and phone with reversible encryption (can decrypt)
51
+ ff1_encrypt :email, :phone, mode: :reversible
52
+
53
+ # Encrypt SSN with irreversible encryption (cannot decrypt - GDPR compliant)
54
+ ff1_encrypt :ssn, mode: :irreversible
55
+
56
+ has_many :posts, dependent: :destroy
57
+ end
58
+
59
+ class Post < ActiveRecord::Base
60
+ include FF1::ActiveRecord
61
+
62
+ # Encrypt post content with reversible encryption
63
+ ff1_encrypt :content, mode: :reversible
64
+
65
+ belongs_to :user
66
+ end
67
+
68
+ # Demonstration
69
+ puts "🔐 FF1 ActiveRecord Integration Demo"
70
+ puts "=" * 50
71
+
72
+ # 1. Create a user with encrypted data
73
+ puts "\n1. Creating user with encrypted data..."
74
+ user = User.create!(
75
+ name: 'John Doe',
76
+ email: 'john@example.com',
77
+ phone: '555-1234',
78
+ ssn: '123-45-6789'
79
+ )
80
+
81
+ puts "✅ User created: #{user.name}"
82
+ puts " Email: #{user.email} (decrypted)"
83
+ puts " Phone: #{user.phone} (decrypted)"
84
+ puts " SSN: #{user.ssn} (irreversible - shows placeholder)"
85
+
86
+ # 2. Show encrypted data in database
87
+ puts "\n2. Data in database (encrypted):"
88
+ raw_data = User.connection.select_one("SELECT email, phone, ssn FROM users WHERE id = #{user.id}")
89
+ puts " Email in DB: #{raw_data['email']} (encrypted)"
90
+ puts " Phone in DB: #{raw_data['phone']} (encrypted)"
91
+ puts " SSN in DB: #{raw_data['ssn']} (encrypted)"
92
+
93
+ # 3. Create a post with encrypted content
94
+ puts "\n3. Creating post with encrypted content..."
95
+ post = Post.create!(
96
+ title: 'Public Title',
97
+ content: 'This is sensitive content that will be encrypted',
98
+ user: user
99
+ )
100
+
101
+ puts "✅ Post created: #{post.title}"
102
+ puts " Content: #{post.content} (decrypted)"
103
+
104
+ raw_post = Post.connection.select_one("SELECT content FROM posts WHERE id = #{post.id}")
105
+ puts " Content in DB: #{raw_post['content']} (encrypted)"
106
+
107
+ # 4. Demonstrate query scopes
108
+ puts "\n4. Query scopes:"
109
+ puts " Total users: #{User.ff1_all.count}"
110
+ puts " Active users: #{User.ff1_active.count}"
111
+ puts " Deleted users: #{User.ff1_deleted.count}"
112
+
113
+ # 5. GDPR-compliant soft delete
114
+ puts "\n5. GDPR-compliant soft delete..."
115
+ puts " Before deletion - email: #{user.email}"
116
+ user.destroy
117
+
118
+ puts "✅ User soft deleted with irreversible encryption"
119
+ puts " After deletion - email: #{user.email} (now shows [ENCRYPTED])"
120
+ puts " Deleted status: #{user.ff1_deleted?}"
121
+ puts " Deleted at: #{user.ff1_deleted_at}"
122
+
123
+ # 6. Query scopes after deletion
124
+ puts "\n6. Query scopes after deletion:"
125
+ puts " Total users: #{User.ff1_all.count}"
126
+ puts " Active users: #{User.ff1_active.count}"
127
+ puts " Deleted users: #{User.ff1_deleted.count}"
128
+
129
+ # 7. Statistics
130
+ puts "\n7. FF1 Statistics:"
131
+ stats = User.ff1_stats
132
+ stats.each do |key, value|
133
+ puts " #{key}: #{value}"
134
+ end
135
+
136
+ # 8. Restore demonstration
137
+ puts "\n8. Restore user (data remains encrypted)..."
138
+ user.ff1_restore
139
+ puts " Deleted status after restore: #{user.ff1_deleted?}"
140
+ puts " Email after restore: #{user.email} (still encrypted)"
141
+
142
+ puts "\n🎉 Demo completed successfully!"
143
+ puts "The FF1 ActiveRecord integration provides seamless encryption"
144
+ puts "with GDPR compliance through irreversible soft deletion."
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FF1
4
+ module ActiveRecord
5
+ # Base functionality for FF1 ActiveRecord integration
6
+ module Base
7
+ extend ActiveSupport::Concern if defined?(ActiveSupport::Concern)
8
+
9
+ # Thread-safe cipher cache
10
+ if defined?(Concurrent::Hash)
11
+ CIPHER_CACHE = Concurrent::Hash.new
12
+ else
13
+ CIPHER_CACHE = {}
14
+ end
15
+
16
+ private
17
+
18
+ # Get or create a cipher for the given column configuration
19
+ def get_cipher_for_column(column, mode = nil)
20
+ config = ff1_config[column.to_sym]
21
+ return nil unless config
22
+
23
+ mode ||= config[:mode]
24
+ key = config[:key] || FF1::ActiveRecord.configuration.global_key
25
+ radix = config[:radix] || determine_radix_for_column(column)
26
+
27
+ raise FF1::Error, "No encryption key provided for column #{column}" unless key
28
+
29
+ cache_key = "#{self.class.name}:#{column}:#{mode}:#{key.hash}:#{radix}"
30
+
31
+ Base::CIPHER_CACHE[cache_key] ||= FF1::Cipher.new(key, radix, mode)
32
+ end
33
+
34
+ # Determine appropriate radix for a column based on its type
35
+ def determine_radix_for_column(column)
36
+ column_type = self.class.columns_hash[column.to_s]&.type
37
+
38
+ case column_type
39
+ when :integer, :bigint, :decimal
40
+ 10 # Numeric columns use base 10
41
+ when :string, :text
42
+ 256 # Text columns use base 256 for full UTF-8 support
43
+ else
44
+ 256 # Default to text mode for unknown types
45
+ end
46
+ end
47
+
48
+ # Generate a tweak for encryption based on model and column
49
+ def generate_tweak_for_column(column)
50
+ # Use model and column name only - don't include record ID to ensure consistency
51
+ # between encryption (before save) and decryption (after save)
52
+ "#{self.class.name}:#{column}"
53
+ end
54
+
55
+ # Validate encryption key format and length
56
+ def validate_encryption_key(key)
57
+ return false unless key.is_a?(String)
58
+ return false unless [16, 24, 32].include?(key.bytesize)
59
+
60
+ true
61
+ end
62
+
63
+ # Handle errors during encryption/decryption
64
+ def handle_encryption_error(error, column, operation)
65
+ Rails.logger.error "FF1 #{operation} error for #{self.class.name}##{column}: #{error.message}" if defined?(Rails) && Rails.respond_to?(:logger)
66
+
67
+ case operation
68
+ when :encrypt
69
+ # For encryption errors, we might want to fail fast
70
+ raise FF1::Error, "Failed to encrypt #{column}: #{error.message}"
71
+ when :decrypt
72
+ # For decryption errors, we might want to return nil or the raw value
73
+ # depending on application requirements
74
+ Rails.logger.warn "Failed to decrypt #{column}, returning raw value" if defined?(Rails) && Rails.respond_to?(:logger)
75
+ read_attribute(column)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FF1
4
+ module ActiveRecord
5
+ # Handles automatic encryption and decryption of configured columns
6
+ module Encryption
7
+ extend ActiveSupport::Concern if defined?(ActiveSupport::Concern)
8
+
9
+ included do
10
+ # Hook into ActiveRecord callbacks
11
+ before_save :encrypt_ff1_columns
12
+ after_save :clear_ff1_decrypted_cache
13
+ after_initialize :setup_ff1_attributes
14
+ after_find :setup_ff1_attributes
15
+ end
16
+
17
+ # Check if an encrypted attribute has changed
18
+ def ff1_attribute_changed?(attr_name)
19
+ if ff1_config.key?(attr_name.to_sym)
20
+ # Check if we have a pending decrypted value (indicating a change)
21
+ decrypted_var = "@ff1_decrypted_#{attr_name}"
22
+ instance_variable_defined?(decrypted_var)
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Encrypt all configured FF1 columns before saving
31
+ def encrypt_ff1_columns
32
+ return unless respond_to?(:ff1_config) && ff1_config
33
+
34
+ ff1_config.each do |column, config|
35
+ # Check if this attribute needs encryption
36
+ decrypted_var = "@ff1_decrypted_#{column}"
37
+ has_decrypted_value = instance_variable_defined?(decrypted_var)
38
+ decrypted_value = instance_variable_get(decrypted_var) if has_decrypted_value
39
+
40
+ # Only encrypt if we have a decrypted value set or this is a new record with a value
41
+ if has_decrypted_value && !decrypted_value.nil?
42
+ encrypted_value = encrypt_attribute_value(column, decrypted_value, config[:mode])
43
+ write_attribute(column, encrypted_value) if encrypted_value
44
+ elsif new_record?
45
+ raw_value = read_attribute(column)
46
+ if raw_value && !raw_value.empty?
47
+ encrypted_value = encrypt_attribute_value(column, raw_value, config[:mode])
48
+ write_attribute(column, encrypted_value) if encrypted_value
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # Setup decrypted attribute values after initialization
55
+ def setup_ff1_attributes
56
+ return unless persisted?
57
+
58
+ ff1_config.each do |column, _config|
59
+ # Store the encrypted value separately so we can access decrypted values
60
+ encrypted_value = read_attribute(column)
61
+ instance_variable_set("@ff1_encrypted_#{column}", encrypted_value)
62
+ end
63
+ end
64
+
65
+ # Clear cached decrypted values after save
66
+ def clear_ff1_decrypted_cache
67
+ ff1_config.keys.each do |column|
68
+ decrypted_var = "@ff1_decrypted_#{column}"
69
+ remove_instance_variable(decrypted_var) if instance_variable_defined?(decrypted_var)
70
+ end
71
+ end
72
+
73
+ # Decrypt an attribute value
74
+ def decrypt_attribute(column)
75
+ return nil unless ff1_config.key?(column.to_sym)
76
+
77
+ config = ff1_config[column.to_sym]
78
+ decrypted_var = "@ff1_decrypted_#{column}"
79
+
80
+ # If in irreversible mode, always return placeholder
81
+ if config[:mode] == FF1::Modes::IRREVERSIBLE
82
+ instance_variable_set(decrypted_var, "[ENCRYPTED]")
83
+ return "[ENCRYPTED]"
84
+ end
85
+
86
+ # If record is soft-deleted, all data should be treated as irreversible
87
+ if ff1_deleted?
88
+ instance_variable_set(decrypted_var, "[ENCRYPTED]")
89
+ return "[ENCRYPTED]"
90
+ end
91
+
92
+ # Return cached decrypted value if available (only for reversible columns)
93
+ return instance_variable_get(decrypted_var) if instance_variable_defined?(decrypted_var)
94
+
95
+ encrypted_value = read_attribute(column)
96
+ return nil if encrypted_value.nil?
97
+ return '' if encrypted_value == ''
98
+
99
+ begin
100
+ cipher = get_cipher_for_column(column, FF1::Modes::REVERSIBLE)
101
+ tweak = generate_tweak_for_column(column)
102
+
103
+ decrypted_value = if text_column?(column)
104
+ cipher.decrypt_text(encrypted_value, tweak)
105
+ else
106
+ cipher.decrypt(encrypted_value, tweak)
107
+ end
108
+
109
+ # Cache the decrypted value
110
+ instance_variable_set(decrypted_var, decrypted_value)
111
+ decrypted_value
112
+ rescue FF1::Error => e
113
+ # If decryption fails (possibly due to irreversible encryption), return placeholder
114
+ instance_variable_set(decrypted_var, "[ENCRYPTED]")
115
+ "[ENCRYPTED]"
116
+ end
117
+ end
118
+
119
+ # Set an encrypted attribute value (stores for later encryption)
120
+ def set_encrypted_attribute(column, value)
121
+ # Clear any existing cached decrypted value
122
+ decrypted_var = "@ff1_decrypted_#{column}"
123
+ remove_instance_variable(decrypted_var) if instance_variable_defined?(decrypted_var)
124
+
125
+ if value.nil?
126
+ # Handle nil values
127
+ instance_variable_set(decrypted_var, nil)
128
+ write_attribute(column, nil)
129
+ else
130
+ # Store the plaintext value for immediate access and later encryption
131
+ instance_variable_set(decrypted_var, value)
132
+
133
+ # Mark the attribute as changed so ActiveRecord knows to save it
134
+ attribute_will_change!(column.to_s) if respond_to?(:attribute_will_change!)
135
+ end
136
+ end
137
+
138
+ # Encrypt a single attribute value
139
+ def encrypt_attribute_value(column, value, mode = nil)
140
+ return nil if value.nil?
141
+ return '' if value == ''
142
+
143
+ config = ff1_config[column.to_sym]
144
+ mode ||= config[:mode]
145
+
146
+ begin
147
+ cipher = get_cipher_for_column(column, mode)
148
+ tweak = generate_tweak_for_column(column)
149
+
150
+ if text_column?(column)
151
+ cipher.encrypt_text(value.to_s, tweak)
152
+ else
153
+ cipher.encrypt(value.to_s, tweak)
154
+ end
155
+ rescue FF1::Error => e
156
+ handle_encryption_error(e, column, :encrypt)
157
+ end
158
+ end
159
+
160
+ # Check if a column is a text-based column
161
+ def text_column?(column)
162
+ column_type = self.class.columns_hash[column.to_s]&.type
163
+ [:string, :text].include?(column_type)
164
+ end
165
+ end
166
+ end
167
+ end