familia 2.0.0.pre6 → 2.0.0.pre7

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -13
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +2 -2
  9. data/docs/wiki/Feature-System-Guide.md +36 -5
  10. data/docs/wiki/Home.md +30 -20
  11. data/docs/wiki/Relationships-Guide.md +684 -0
  12. data/examples/bit_encoding_integration.rb +237 -0
  13. data/examples/redis_command_validation_example.rb +231 -0
  14. data/examples/relationships_basic.rb +273 -0
  15. data/lib/familia/connection.rb +3 -3
  16. data/lib/familia/data_type.rb +7 -4
  17. data/lib/familia/features/encrypted_fields/concealed_string.rb +21 -23
  18. data/lib/familia/features/encrypted_fields.rb +413 -4
  19. data/lib/familia/features/expiration.rb +319 -33
  20. data/lib/familia/features/quantization.rb +385 -44
  21. data/lib/familia/features/relationships/cascading.rb +438 -0
  22. data/lib/familia/features/relationships/indexing.rb +370 -0
  23. data/lib/familia/features/relationships/membership.rb +503 -0
  24. data/lib/familia/features/relationships/permission_management.rb +264 -0
  25. data/lib/familia/features/relationships/querying.rb +620 -0
  26. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  27. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  28. data/lib/familia/features/relationships/tracking.rb +379 -0
  29. data/lib/familia/features/relationships.rb +466 -0
  30. data/lib/familia/features/transient_fields.rb +192 -10
  31. data/lib/familia/features.rb +2 -1
  32. data/lib/familia/horreum/subclass/definition.rb +1 -1
  33. data/lib/familia/validation/command_recorder.rb +336 -0
  34. data/lib/familia/validation/expectations.rb +519 -0
  35. data/lib/familia/validation/test_helpers.rb +443 -0
  36. data/lib/familia/validation/validator.rb +412 -0
  37. data/lib/familia/validation.rb +140 -0
  38. data/lib/familia/version.rb +1 -1
  39. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  40. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  41. data/try/edge_cases/string_coercion_try.rb +2 -0
  42. data/try/encryption/encryption_core_try.rb +3 -1
  43. data/try/features/categorical_permissions_try.rb +515 -0
  44. data/try/features/encryption_fields/concealed_string_core_try.rb +3 -0
  45. data/try/features/encryption_fields/context_isolation_try.rb +1 -0
  46. data/try/features/relationships_edge_cases_try.rb +145 -0
  47. data/try/features/relationships_performance_minimal_try.rb +132 -0
  48. data/try/features/relationships_performance_simple_try.rb +155 -0
  49. data/try/features/relationships_performance_try.rb +420 -0
  50. data/try/features/relationships_performance_working_try.rb +144 -0
  51. data/try/features/relationships_try.rb +237 -0
  52. data/try/features/safe_dump_try.rb +3 -0
  53. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  54. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  55. data/try/helpers/test_helpers.rb +1 -1
  56. data/try/horreum/base_try.rb +14 -8
  57. data/try/horreum/enhanced_conflict_handling_try.rb +2 -0
  58. data/try/horreum/relations_try.rb +1 -1
  59. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  60. data/try/validation/command_validation_try.rb.disabled +207 -0
  61. data/try/validation/performance_validation_try.rb.disabled +324 -0
  62. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  63. metadata +32 -4
  64. data/docs/wiki/RelatableObjects-Guide.md +0 -563
  65. data/lib/familia/features/relatable_objects.rb +0 -125
  66. data/try/features/relatable_objects_try.rb +0 -220
@@ -4,22 +4,431 @@ require_relative 'encrypted_fields/encrypted_field_type'
4
4
 
5
5
  module Familia
6
6
  module Features
7
+ # EncryptedFields is a feature that provides transparent encryption and decryption
8
+ # of sensitive data stored in Redis/Valkey. It uses strong cryptographic algorithms
9
+ # with field-specific key derivation to protect data at rest while maintaining
10
+ # easy access patterns for authorized applications.
11
+ #
12
+ # This feature automatically encrypts field values before storage and decrypts
13
+ # them on access, providing seamless integration with existing code while
14
+ # ensuring sensitive data is never stored in plaintext.
15
+ #
16
+ # Supported Encryption Algorithms:
17
+ # - XChaCha20-Poly1305 (preferred, requires rbnacl gem)
18
+ # - AES-256-GCM (fallback, uses OpenSSL)
19
+ #
20
+ # Example:
21
+ #
22
+ # class Vault < Familia::Horreum
23
+ # feature :encrypted_fields
24
+ #
25
+ # field :name # Regular unencrypted field
26
+ # encrypted_field :secret_key # Encrypted storage
27
+ # encrypted_field :api_token # Another encrypted field
28
+ # encrypted_field :love_letter # Ultra-sensitive field
29
+ # end
30
+ #
31
+ # vault = Vault.new(
32
+ # name: "Production Vault",
33
+ # secret_key: "super-secret-key-123",
34
+ # api_token: "sk-1234567890abcdef",
35
+ # love_letter: "Dear Alice, I love you. -Bob"
36
+ # )
37
+ #
38
+ # vault.save
39
+ # # Only 'name' is stored in plaintext
40
+ # # secret_key, api_token, love_letter are encrypted
41
+ #
42
+ # # Access is transparent
43
+ # vault.secret_key # => "super-secret-key-123" (decrypted automatically)
44
+ # vault.api_token # => "sk-1234567890abcdef" (decrypted automatically)
45
+ #
46
+ # Security Features:
47
+ #
48
+ # Each encrypted field uses a unique encryption key derived from:
49
+ # - Master encryption key (from Familia.encryption_key)
50
+ # - Field name (cryptographic domain separation)
51
+ # - Record identifier (per-record key derivation)
52
+ # - Class name (per-class key derivation)
53
+ #
54
+ # This ensures that:
55
+ # - Swapping encrypted values between fields fails to decrypt
56
+ # - Each record has unique encryption keys
57
+ # - Different classes cannot decrypt each other's data
58
+ # - Field-level access control is cryptographically enforced
59
+ #
60
+ # Cryptographic Design:
61
+ #
62
+ # # XChaCha20-Poly1305 (preferred)
63
+ # - 256-bit keys (32 bytes)
64
+ # - 192-bit nonces (24 bytes) - extended nonce space
65
+ # - 128-bit authentication tags (16 bytes)
66
+ # - BLAKE2b key derivation with personalization
67
+ #
68
+ # # AES-256-GCM (fallback)
69
+ # - 256-bit keys (32 bytes)
70
+ # - 96-bit nonces (12 bytes) - standard GCM nonce
71
+ # - 128-bit authentication tags (16 bytes)
72
+ # - HKDF-SHA256 key derivation
73
+ #
74
+ # Ciphertext Format:
75
+ #
76
+ # Encrypted data is stored as JSON with algorithm-specific metadata:
77
+ #
78
+ # {
79
+ # "algorithm": "xchacha20poly1305",
80
+ # "nonce": "base64_encoded_nonce",
81
+ # "ciphertext": "base64_encoded_data",
82
+ # "auth_tag": "base64_encoded_tag",
83
+ # "key_version": "v1"
84
+ # }
85
+ #
86
+ # Additional Authenticated Data (AAD):
87
+ #
88
+ # For extra security, you can include other field values in the authentication:
89
+ #
90
+ # class SecureDocument < Familia::Horreum
91
+ # feature :encrypted_fields
92
+ #
93
+ # field :doc_id, :owner_id, :classification
94
+ # encrypted_field :content, aad_fields: [:doc_id, :owner_id, :classification]
95
+ # end
96
+ #
97
+ # # The content can only be decrypted if doc_id, owner_id, and classification
98
+ # # values match those used during encryption
99
+ #
100
+ # Passphrase Protection:
101
+ #
102
+ # For ultra-sensitive fields, require user passphrases for decryption:
103
+ #
104
+ # class PersonalVault < Familia::Horreum
105
+ # feature :encrypted_fields
106
+ #
107
+ # field :user_id
108
+ # encrypted_field :diary_entry # Ultra-sensitive
109
+ # encrypted_field :photos # Ultra-sensitive
110
+ # end
111
+ #
112
+ # vault = PersonalVault.new(user_id: 123, diary_entry: "Dear diary...")
113
+ # vault.save
114
+ #
115
+ # # Passphrase required for decryption
116
+ # diary = vault.diary_entry(passphrase_value: user_passphrase)
117
+ #
118
+ # Memory Safety:
119
+ #
120
+ # Encrypted fields return ConcealedString objects that provide memory protection:
121
+ #
122
+ # secret = vault.secret_key
123
+ # secret.class # => ConcealedString
124
+ # puts secret # => "[CONCEALED]" (automatic redaction)
125
+ # secret.inspect # => "[CONCEALED]" (automatic redaction)
126
+ #
127
+ # # Safe access pattern
128
+ # secret.expose do |value|
129
+ # # Use value directly without creating copies
130
+ # api_call(authorization: "Bearer #{value}")
131
+ # end
132
+ #
133
+ # # Direct access (use carefully)
134
+ # raw_value = secret.value # Returns actual decrypted string
135
+ #
136
+ # # Explicit cleanup
137
+ # secret.clear! # Best-effort memory wiping
138
+ #
139
+ # Error Handling:
140
+ #
141
+ # The feature provides specific error types for different failure modes:
142
+ #
143
+ # # Invalid ciphertext or tampering
144
+ # vault.secret_key # => Familia::EncryptionError: Authentication failed
145
+ #
146
+ # # Wrong passphrase
147
+ # vault.diary_entry(passphrase_value: "wrong")
148
+ # # => Familia::EncryptionError: Invalid passphrase
149
+ #
150
+ # # Missing encryption key
151
+ # Familia.encryption_key = nil
152
+ # vault.secret_key # => Familia::EncryptionError: No encryption key configured
153
+ #
154
+ # Configuration:
155
+ #
156
+ # # Set master encryption key (required)
157
+ # Familia.configure do |config|
158
+ # config.encryption_key = ENV['FAMILIA_ENCRYPTION_KEY']
159
+ # config.encryption_personalization = 'MyApp-2024' # Optional customization
160
+ # end
161
+ #
162
+ # # Generate a new encryption key
163
+ # key = Familia::Encryption.generate_key
164
+ # puts key # => "base64-encoded-32-byte-key"
165
+ #
166
+ # Key Rotation:
167
+ #
168
+ # The feature supports key versioning for seamless key rotation:
169
+ #
170
+ # # Step 1: Add new key while keeping old key
171
+ # Familia.configure do |config|
172
+ # config.encryption_key = new_key
173
+ # config.legacy_encryption_keys = { 'v1' => old_key }
174
+ # end
175
+ #
176
+ # # Step 2: Objects decrypt with old key, encrypt with new key
177
+ # vault.secret_key = "new-secret" # Encrypted with new key
178
+ # vault.save
179
+ #
180
+ # # Step 3: After all data is re-encrypted, remove legacy key
181
+ #
182
+ # Integration Patterns:
183
+ #
184
+ # # Rails application
185
+ # class User < ApplicationRecord
186
+ # include Familia::Horreum
187
+ # feature :encrypted_fields
188
+ #
189
+ # field :user_id
190
+ # encrypted_field :credit_card_number
191
+ # encrypted_field :ssn, aad_fields: [:user_id]
192
+ # end
193
+ #
194
+ # # API serialization (encrypted fields excluded by default)
195
+ # class UserSerializer
196
+ # def self.serialize(user)
197
+ # {
198
+ # id: user.user_id,
199
+ # created_at: user.created_at,
200
+ # # credit_card_number and ssn are NOT included
201
+ # }
202
+ # end
203
+ # end
204
+ #
205
+ # # Background job processing
206
+ # class PaymentProcessor
207
+ # def process_payment(user_id)
208
+ # user = User.find(user_id)
209
+ #
210
+ # # Access encrypted field safely
211
+ # user.credit_card_number.expose do |cc_number|
212
+ # # Process payment without storing plaintext
213
+ # payment_gateway.charge(cc_number, amount)
214
+ # end
215
+ #
216
+ # # Clear sensitive data from memory
217
+ # user.credit_card_number.clear!
218
+ # end
219
+ # end
220
+ #
221
+ # Performance Considerations:
222
+ #
223
+ # - Encryption/decryption adds ~1-5ms overhead per field
224
+ # - Key derivation is cached per field/record combination
225
+ # - XChaCha20-Poly1305 is ~2x faster than AES-256-GCM
226
+ # - Memory allocation increases due to ciphertext expansion
227
+ # - Consider batching operations for high-throughput scenarios
228
+ #
229
+ # Security Limitations:
230
+ #
231
+ # ⚠️ Important: Ruby provides NO memory safety guarantees:
232
+ # - No secure memory wiping (best-effort only)
233
+ # - Garbage collector may copy secrets
234
+ # - String operations create uncontrolled copies
235
+ # - Memory dumps may contain plaintext secrets
236
+ #
237
+ # For highly sensitive applications, consider:
238
+ # - External key management (HashiCorp Vault, AWS KMS)
239
+ # - Hardware Security Modules (HSMs)
240
+ # - Languages with secure memory handling
241
+ # - Dedicated cryptographic appliances
242
+ #
243
+ # Threat Model:
244
+ #
245
+ # ✅ Protected Against:
246
+ # - Database compromise (encrypted data only)
247
+ # - Field value swapping (field-specific keys)
248
+ # - Cross-record attacks (record-specific keys)
249
+ # - Tampering (authenticated encryption)
250
+ #
251
+ # ❌ Not Protected Against:
252
+ # - Master key compromise (all data compromised)
253
+ # - Application memory compromise (plaintext in RAM)
254
+ # - Side-channel attacks (timing, power analysis)
255
+ # - Insider threats with application access
256
+ #
7
257
  module EncryptedFields
8
258
  def self.included(base)
259
+ Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
9
260
  base.extend ClassMethods
261
+
262
+ # Initialize encrypted fields tracking
263
+ base.instance_variable_set(:@encrypted_fields, []) unless base.instance_variable_defined?(:@encrypted_fields)
10
264
  end
11
265
 
12
266
  module ClassMethods
13
- # Define an encrypted field
267
+ # Define an encrypted field that transparently encrypts/decrypts values
268
+ #
269
+ # Encrypted fields are stored as JSON objects containing the encrypted
270
+ # ciphertext along with cryptographic metadata. Values are automatically
271
+ # encrypted on assignment and decrypted on access.
272
+ #
14
273
  # @param name [Symbol] Field name
15
- # @param aad_fields [Array<Symbol>] Optional fields to include in AAD
274
+ # @param aad_fields [Array<Symbol>] Additional fields to include in authentication
16
275
  # @param kwargs [Hash] Additional field options
17
- def encrypted_field(name, aad_fields: [], **)
276
+ #
277
+ # @example Basic encrypted field
278
+ # class Vault < Familia::Horreum
279
+ # feature :encrypted_fields
280
+ # encrypted_field :secret_key
281
+ # end
282
+ #
283
+ # @example Encrypted field with additional authentication
284
+ # class Document < Familia::Horreum
285
+ # feature :encrypted_fields
286
+ # field :doc_id, :owner_id
287
+ # encrypted_field :content, aad_fields: [:doc_id, :owner_id]
288
+ # end
289
+ #
290
+ def encrypted_field(name, aad_fields: [], **kwargs)
291
+ @encrypted_fields ||= []
292
+ @encrypted_fields << name unless @encrypted_fields.include?(name)
293
+
18
294
  require_relative 'encrypted_fields/encrypted_field_type'
19
295
 
20
- field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **)
296
+ field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **kwargs)
21
297
  register_field_type(field_type)
22
298
  end
299
+
300
+ # Returns list of encrypted field names defined on this class
301
+ #
302
+ # @return [Array<Symbol>] Array of encrypted field names
303
+ #
304
+ def encrypted_fields
305
+ @encrypted_fields || []
306
+ end
307
+
308
+ # Check if a field is encrypted
309
+ #
310
+ # @param field_name [Symbol] The field name to check
311
+ # @return [Boolean] true if field is encrypted, false otherwise
312
+ #
313
+ def encrypted_field?(field_name)
314
+ encrypted_fields.include?(field_name.to_sym)
315
+ end
316
+
317
+ # Get encryption algorithm information
318
+ #
319
+ # @return [Hash] Hash containing encryption algorithm details
320
+ #
321
+ def encryption_info
322
+ provider = Familia::Encryption.current_provider
323
+ {
324
+ algorithm: provider.algorithm_name,
325
+ key_size: provider.key_size,
326
+ nonce_size: provider.nonce_size,
327
+ tag_size: provider.tag_size
328
+ }
329
+ end
330
+ end
331
+
332
+ # Check if this instance has any encrypted fields with values
333
+ #
334
+ # @return [Boolean] true if any encrypted fields have values
335
+ #
336
+ # TODO: Missing test coverage
337
+ def encrypted_data?
338
+ self.class.encrypted_fields.any? do |field_name|
339
+ field_value = instance_variable_get("@#{field_name}")
340
+ !field_value.nil?
341
+ end
342
+ end
343
+
344
+ # Clear all encrypted field values from memory
345
+ #
346
+ # This method iterates through all encrypted fields and calls clear!
347
+ # on any ConcealedString instances. Use this for cleanup when the
348
+ # object is no longer needed.
349
+ #
350
+ # @return [void]
351
+ #
352
+ # @example Clear all secrets when done
353
+ # vault = Vault.new(secret_key: 'secret', api_token: 'token123')
354
+ # # ... use vault ...
355
+ # vault.clear_encrypted_fields!
356
+ #
357
+ def clear_encrypted_fields!
358
+ self.class.encrypted_fields.each do |field_name|
359
+ field_value = instance_variable_get("@#{field_name}")
360
+ if field_value.respond_to?(:clear!)
361
+ field_value.clear!
362
+ end
363
+ end
364
+ end
365
+
366
+ # Check if all encrypted fields have been cleared from memory
367
+ #
368
+ # @return [Boolean] true if all encrypted fields are cleared, false otherwise
369
+ #
370
+ def encrypted_fields_cleared?
371
+ self.class.encrypted_fields.all? do |field_name|
372
+ field_value = instance_variable_get("@#{field_name}")
373
+ field_value.nil? || (field_value.respond_to?(:cleared?) && field_value.cleared?)
374
+ end
375
+ end
376
+
377
+ # Re-encrypt all encrypted fields with current encryption settings
378
+ #
379
+ # This method is useful for key rotation or algorithm upgrades.
380
+ # It decrypts all encrypted fields and re-encrypts them with the
381
+ # current encryption configuration.
382
+ #
383
+ # @return [Boolean] true if re-encryption succeeded
384
+ #
385
+ # @example Re-encrypt after key rotation
386
+ # vault.re_encrypt_fields!
387
+ # vault.save
388
+ #
389
+ def re_encrypt_fields!
390
+ self.class.encrypted_fields.each do |field_name|
391
+ current_value = send(field_name)
392
+ next if current_value.nil?
393
+
394
+ # Force re-encryption by setting the value again
395
+ if current_value.respond_to?(:value)
396
+ send("#{field_name}=", current_value.value)
397
+ else
398
+ send("#{field_name}=", current_value)
399
+ end
400
+ end
401
+ true
402
+ end
403
+
404
+ # Get encryption status for all encrypted fields
405
+ #
406
+ # Returns a hash showing the encryption status of each encrypted field,
407
+ # useful for debugging and monitoring.
408
+ #
409
+ # @return [Hash] Hash with field names as keys and status information
410
+ #
411
+ # @example Check encryption status
412
+ # vault.encrypted_fields_status
413
+ # # => {
414
+ # # secret_key: { encrypted: true, algorithm: "xchacha20poly1305", cleared: false },
415
+ # # api_token: { encrypted: true, algorithm: "aes-256-gcm", cleared: true }
416
+ # # }
417
+ #
418
+ def encrypted_fields_status
419
+ self.class.encrypted_fields.each_with_object({}) do |field_name, status|
420
+ field_value = instance_variable_get("@#{field_name}")
421
+
422
+ if field_value.nil?
423
+ status[field_name] = { encrypted: false, value: nil }
424
+ elsif field_value.respond_to?(:cleared?) && field_value.cleared?
425
+ status[field_name] = { encrypted: true, cleared: true }
426
+ elsif field_value.respond_to?(:concealed?) && field_value.concealed?
427
+ status[field_name] = { encrypted: true, algorithm: "unknown", cleared: false }
428
+ else
429
+ status[field_name] = { encrypted: false, value: "[CONCEALED]" }
430
+ end
431
+ end
23
432
  end
24
433
 
25
434
  Familia::Base.add_feature self, :encrypted_fields