familia 2.0.0.pre4 → 2.0.0.pre6

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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +11 -8
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +19 -3
  7. data/README.md +36 -157
  8. data/docs/overview.md +359 -0
  9. data/docs/wiki/API-Reference.md +347 -0
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +600 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +106 -0
  17. data/docs/wiki/Implementation-Guide.md +276 -0
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  20. data/docs/wiki/Security-Model.md +183 -0
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/lib/familia/base.rb +18 -27
  23. data/lib/familia/connection.rb +6 -5
  24. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  25. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  26. data/lib/familia/data_type/types/counter.rb +38 -0
  27. data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
  28. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  31. data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
  32. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  33. data/lib/familia/{datatype.rb → data_type.rb} +12 -14
  34. data/lib/familia/encryption/encrypted_data.rb +137 -0
  35. data/lib/familia/encryption/manager.rb +119 -0
  36. data/lib/familia/encryption/provider.rb +49 -0
  37. data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
  38. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  39. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
  40. data/lib/familia/encryption/registry.rb +50 -0
  41. data/lib/familia/encryption.rb +178 -0
  42. data/lib/familia/encryption_request_cache.rb +68 -0
  43. data/lib/familia/errors.rb +17 -3
  44. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  45. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
  46. data/lib/familia/features/encrypted_fields.rb +28 -0
  47. data/lib/familia/features/expiration.rb +107 -77
  48. data/lib/familia/features/quantization.rb +5 -9
  49. data/lib/familia/features/relatable_objects.rb +2 -4
  50. data/lib/familia/features/safe_dump.rb +14 -17
  51. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  52. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  53. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  54. data/lib/familia/features/transient_fields.rb +47 -0
  55. data/lib/familia/features.rb +40 -24
  56. data/lib/familia/field_type.rb +273 -0
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
  58. data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
  63. data/lib/familia/horreum/subclass/definition.rb +469 -0
  64. data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +30 -22
  67. data/lib/familia/logging.rb +14 -14
  68. data/lib/familia/settings.rb +39 -3
  69. data/lib/familia/utils.rb +45 -0
  70. data/lib/familia/version.rb +1 -1
  71. data/lib/familia.rb +3 -2
  72. data/try/core/base_enhancements_try.rb +115 -0
  73. data/try/core/connection_try.rb +0 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -5
  77. data/try/core/familia_extended_try.rb +3 -4
  78. data/try/core/familia_try.rb +1 -2
  79. data/try/core/persistence_operations_try.rb +297 -0
  80. data/try/core/pools_try.rb +2 -2
  81. data/try/core/secure_identifier_try.rb +0 -1
  82. data/try/core/settings_try.rb +0 -1
  83. data/try/core/utils_try.rb +0 -1
  84. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  85. data/try/data_types/counter_try.rb +93 -0
  86. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  87. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  88. data/try/{datatypes → data_types}/list_try.rb +1 -2
  89. data/try/data_types/lock_try.rb +133 -0
  90. data/try/{datatypes → data_types}/set_try.rb +1 -2
  91. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  92. data/try/{datatypes → data_types}/string_try.rb +1 -2
  93. data/try/debugging/README.md +32 -0
  94. data/try/debugging/cache_behavior_tracer.rb +91 -0
  95. data/try/debugging/debug_aad_process.rb +82 -0
  96. data/try/debugging/debug_concealed_internal.rb +59 -0
  97. data/try/debugging/debug_concealed_reveal.rb +61 -0
  98. data/try/debugging/debug_context_aad.rb +68 -0
  99. data/try/debugging/debug_context_simple.rb +80 -0
  100. data/try/debugging/debug_cross_context.rb +62 -0
  101. data/try/debugging/debug_database_load.rb +64 -0
  102. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  103. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  104. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  105. data/try/debugging/debug_field_decrypt.rb +74 -0
  106. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  107. data/try/debugging/debug_load_path.rb +66 -0
  108. data/try/debugging/debug_method_definition.rb +46 -0
  109. data/try/debugging/debug_method_resolution.rb +41 -0
  110. data/try/debugging/debug_minimal.rb +24 -0
  111. data/try/debugging/debug_provider.rb +68 -0
  112. data/try/debugging/debug_secure_behavior.rb +73 -0
  113. data/try/debugging/debug_string_class.rb +46 -0
  114. data/try/debugging/debug_test.rb +46 -0
  115. data/try/debugging/debug_test_design.rb +80 -0
  116. data/try/debugging/encryption_method_tracer.rb +138 -0
  117. data/try/debugging/provider_diagnostics.rb +110 -0
  118. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  119. data/try/edge_cases/json_serialization_try.rb +0 -1
  120. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  121. data/try/encryption/config_persistence_try.rb +192 -0
  122. data/try/encryption/encryption_core_try.rb +328 -0
  123. data/try/encryption/instance_variable_scope_try.rb +31 -0
  124. data/try/encryption/module_loading_try.rb +28 -0
  125. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  126. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  127. data/try/encryption/roundtrip_validation_try.rb +28 -0
  128. data/try/encryption/secure_memory_handling_try.rb +125 -0
  129. data/try/features/encrypted_fields_core_try.rb +125 -0
  130. data/try/features/encrypted_fields_integration_try.rb +216 -0
  131. data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
  132. data/try/features/encrypted_fields_security_try.rb +377 -0
  133. data/try/features/encryption_fields/aad_protection_try.rb +138 -0
  134. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  135. data/try/features/encryption_fields/context_isolation_try.rb +141 -0
  136. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  137. data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
  138. data/try/features/encryption_fields/fresh_key_try.rb +168 -0
  139. data/try/features/encryption_fields/key_rotation_try.rb +123 -0
  140. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  141. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  142. data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
  143. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  144. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  145. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  146. data/try/features/expiration_try.rb +0 -1
  147. data/try/features/feature_dependencies_try.rb +159 -0
  148. data/try/features/quantization_try.rb +0 -1
  149. data/try/features/real_feature_integration_try.rb +148 -0
  150. data/try/features/relatable_objects_try.rb +0 -1
  151. data/try/features/safe_dump_advanced_try.rb +0 -1
  152. data/try/features/safe_dump_try.rb +0 -1
  153. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  154. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  155. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  156. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  157. data/try/features/transient_fields_core_try.rb +181 -0
  158. data/try/features/transient_fields_integration_try.rb +260 -0
  159. data/try/helpers/test_helpers.rb +67 -0
  160. data/try/horreum/base_try.rb +157 -3
  161. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  162. data/try/horreum/field_categories_try.rb +118 -0
  163. data/try/horreum/field_definition_try.rb +96 -0
  164. data/try/horreum/initialization_try.rb +1 -2
  165. data/try/horreum/relations_try.rb +1 -2
  166. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  167. data/try/horreum/serialization_try.rb +41 -7
  168. data/try/memory/memory_basic_test.rb +73 -0
  169. data/try/memory/memory_detailed_test.rb +121 -0
  170. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  171. data/try/memory/memory_search_for_string.rb +83 -0
  172. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  173. data/try/models/customer_safe_dump_try.rb +1 -2
  174. data/try/models/customer_try.rb +1 -2
  175. data/try/models/datatype_base_try.rb +1 -2
  176. data/try/models/familia_object_try.rb +0 -1
  177. metadata +131 -23
  178. data/lib/familia/horreum/serialization.rb +0 -445
@@ -0,0 +1,310 @@
1
+ # try/features/encryption_fields/secure_by_default_behavior_try.rb
2
+
3
+ require_relative '../../helpers/test_helpers'
4
+ require 'base64'
5
+
6
+ Familia.debug = false
7
+
8
+ # Configure encryption keys
9
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
10
+ Familia.config.encryption_keys = test_keys
11
+ Familia.config.current_key_version = :v1
12
+
13
+ # Test class demonstrating secure patterns
14
+ class SecureUserAccount < Familia::Horreum
15
+ feature :encrypted_fields
16
+ identifier_field :id
17
+ field :id
18
+ field :username # Public field
19
+ field :email # Public field
20
+ encrypted_field :password_hash # Secure field
21
+ encrypted_field :api_secret # Secure field
22
+ encrypted_field :recovery_key # Secure field
23
+ end
24
+
25
+ # Clean test environment
26
+ Familia.dbclient.flushdb
27
+
28
+ # Create test user
29
+ @user = SecureUserAccount.new
30
+ @user.id = "user123"
31
+ @user.username = "john_doe"
32
+ @user.email = "john@example.com"
33
+ @user.password_hash = "bcrypt$2a$12$abcdef..."
34
+ @user.api_secret = "sk-1234567890abcdef"
35
+ @user.recovery_key = "recovery-key-xyz789"
36
+
37
+ ## Public field returns String class and value
38
+ @user.username
39
+ #=:> String
40
+ #=> "john_doe"
41
+
42
+ ## Email field returns correct value
43
+ @user.email
44
+ #=> "john@example.com"
45
+
46
+ ## Password hash returns ConcealedString - no auto-decryption
47
+ @user.password_hash.class.name
48
+ #=> "ConcealedString"
49
+
50
+ ## API secret returns ConcealedString
51
+ @user.api_secret.class.name
52
+ #=> "ConcealedString"
53
+
54
+ ## Recovery key returns ConcealedString
55
+ @user.recovery_key.class.name
56
+ #=> "ConcealedString"
57
+
58
+ ## Explicit reveal required for password access
59
+ revealed_password = nil
60
+ @user.password_hash.reveal do |plaintext|
61
+ revealed_password = plaintext
62
+ end
63
+ revealed_password
64
+ #=> "bcrypt$2a$12$abcdef..."
65
+
66
+ ## Multiple encrypted fields require individual reveals
67
+ revealed_secret = nil
68
+ @user.api_secret.reveal do |plaintext|
69
+ revealed_secret = plaintext
70
+ end
71
+ revealed_secret
72
+ #=> "sk-1234567890abcdef"
73
+
74
+ ## String operations on encrypted fields are safe
75
+ password_string = @user.password_hash.to_s
76
+ password_string.include?("bcrypt")
77
+ #=> false
78
+
79
+ ## to_s returns concealed marker
80
+ @user.password_hash.to_s
81
+ #=> "[CONCEALED]"
82
+
83
+ ## inspect is safe for debugging
84
+ inspect_result = @user.password_hash.inspect
85
+ inspect_result.include?("bcrypt")
86
+ #=> false
87
+
88
+ ## inspect returns concealed marker
89
+ @user.password_hash.inspect
90
+ #=> "[CONCEALED]"
91
+
92
+ ## Array operations are secure
93
+ all_fields = [@user.username, @user.password_hash, @user.api_secret]
94
+ field_strings = all_fields.map(&:to_s)
95
+ field_strings
96
+ #=> ["john_doe", "[CONCEALED]", "[CONCEALED]"]
97
+
98
+ ## Hash serialization excludes encrypted fields
99
+ user_hash = @user.to_h
100
+ user_hash.keys.include?("password_hash")
101
+ #=> false
102
+
103
+ ## API secret also excluded from serialization
104
+ @user.to_h.keys.include?("api_secret")
105
+ #=> false
106
+
107
+ ## Recovery key excluded from serialization
108
+ @user.to_h.keys.include?("recovery_key")
109
+ #=> false
110
+
111
+ ## Only public fields included in serialization
112
+ @user.to_h.keys.sort
113
+ #=> ["email", "id", "username"]
114
+
115
+ ## Database operations preserve security
116
+ @user.save
117
+ #=> true
118
+
119
+ ## Fresh load from database returns ConcealedString
120
+ @fresh_user = SecureUserAccount.load("user123")
121
+ @fresh_user.password_hash.class.name
122
+ #=> "ConcealedString"
123
+
124
+ ## Fresh user API secret is concealed
125
+ @fresh_user.api_secret.class.name
126
+ #=> "ConcealedString"
127
+
128
+ ## Plaintext still requires explicit reveal after reload
129
+ revealed_fresh = nil
130
+ @fresh_user.password_hash.reveal do |plaintext|
131
+ revealed_fresh = plaintext
132
+ end
133
+ revealed_fresh
134
+ #=> "bcrypt$2a$12$abcdef..."
135
+
136
+ ## Regular field string operations work normally
137
+ @user.username.upcase
138
+ #=> "JOHN_DOE"
139
+
140
+ ## Regular field length access
141
+ @user.username.length
142
+ #=> 8
143
+
144
+ ## Regular field substring access
145
+ @user.username[0..3]
146
+ #=> "john"
147
+
148
+ ## Encrypted field string operations are protected
149
+ @user.password_hash.upcase
150
+ #=> "[CONCEALED]"
151
+
152
+ ## Encrypted field length is concealed length
153
+ @user.password_hash.length
154
+ #=> 11
155
+
156
+ ## Encrypted field substring is from concealed text
157
+ @user.password_hash.to_s[0..3]
158
+ #=> "[CON"
159
+
160
+ ## Logging patterns are safe
161
+ @log_message = "User #{@user.username} with password #{@user.password_hash}"
162
+ @log_message.include?("bcrypt")
163
+ #=> false
164
+
165
+ ## Log message shows concealed value
166
+ @log_message
167
+ #=> "User john_doe with password [CONCEALED]"
168
+
169
+ ## Exception messages are safe
170
+ begin
171
+ raise StandardError, "Authentication failed for #{@user.password_hash}"
172
+ rescue StandardError => e
173
+ e.message.include?("bcrypt")
174
+ end
175
+ #=> false
176
+
177
+ ## String method chaining is safe
178
+ transformed = @user.password_hash.downcase.strip.gsub(/secret/, "public")
179
+ transformed
180
+ #=> "[CONCEALED]"
181
+
182
+ ## Concatenation fails safely without to_str method (prevents accidental implicit conversion)
183
+ begin
184
+ @combined = "Password: " + @user.password_hash + " (encrypted)"
185
+ "concatenation_should_fail"
186
+ rescue TypeError => e
187
+ e.message.include?("no implicit conversion")
188
+ end
189
+ #=> true
190
+
191
+ ## JSON serialization is safe
192
+ @user_json = {
193
+ id: @user.id,
194
+ username: @user.username,
195
+ password: @user.password_hash
196
+ }.to_json
197
+
198
+ @user_json.include?("bcrypt")
199
+ #=> false
200
+
201
+ ## JSON contains concealed marker
202
+ @user_json.include?("[CONCEALED]")
203
+ #=> true
204
+
205
+ ## Bulk field operations are secure
206
+ @encrypted_fields = [:password_hash, :api_secret, :recovery_key]
207
+ @field_values = @encrypted_fields.map { |field| @user.send(field) }
208
+
209
+ @field_values.all? { |val| val.class.name == "ConcealedString" }
210
+ #=> true
211
+
212
+ ## All serialize safely
213
+ @safe_strings = @field_values.map(&:to_s)
214
+ @safe_strings.all? { |str| str == "[CONCEALED]" }
215
+ #=> true
216
+
217
+ ## Conditional operations are safe
218
+ password_present = @user.password_hash.present?
219
+ password_present
220
+ #=> true
221
+
222
+ ## Empty check is safe
223
+ password_empty = @user.password_hash.empty?
224
+ password_empty
225
+ #=> false
226
+
227
+ ## Nil check works
228
+ (@user.password_hash.nil?)
229
+ #=> false
230
+
231
+ ## Assignment maintains security
232
+ @user.api_secret = "new-secret-key-456"
233
+ @user.api_secret.class.name
234
+ #=> "ConcealedString"
235
+
236
+ ## New assignment serializes safely
237
+ @user.api_secret.to_s
238
+ #=> "[CONCEALED]"
239
+
240
+ ## Updating with reveal access to old value
241
+ old_value = nil
242
+ new_value = "updated-secret-789"
243
+
244
+ @user.api_secret.reveal do |current|
245
+ old_value = current
246
+ @user.api_secret = new_value
247
+ end
248
+
249
+ old_value
250
+ #=> "new-secret-key-456"
251
+
252
+ ## Updated value accessible via reveal
253
+ @user.api_secret.reveal { |x| x }
254
+ #=> "updated-secret-789"
255
+
256
+ ## Nil assignment returns nil
257
+ @user.recovery_key = nil
258
+ @user.recovery_key
259
+ #=> nil
260
+
261
+ ## Nil encrypted fields are NilClass
262
+ @user.recovery_key.class.name
263
+ #=> "NilClass"
264
+
265
+ ## Setting back to value returns to ConcealedString
266
+ @user.recovery_key = "new-recovery-key"
267
+ @user.recovery_key.class.name
268
+ #=> "ConcealedString"
269
+
270
+ ## Common mistake patterns are secure - string interpolation
271
+ search_pattern = "password_hash:#{@user.password_hash}"
272
+ search_pattern.include?("bcrypt")
273
+ #=> false
274
+
275
+ ## API response safety
276
+ api_response = {
277
+ user_id: @user.id,
278
+ credentials: {
279
+ password: @user.password_hash,
280
+ api_key: @user.api_secret
281
+ }
282
+ }
283
+
284
+ @response_json = api_response.to_json
285
+ @response_json.include?("bcrypt")
286
+ #=> false
287
+
288
+ ## API response doesn't leak secrets
289
+ @response_json.include?("sk-1234567890abcdef")
290
+ #=> false
291
+
292
+ ## API response contains concealed markers
293
+ @response_json.include?("[CONCEALED]")
294
+ #=> true
295
+
296
+ ## Debug logging safety
297
+ @debug_values = @user.instance_variables.map do |var|
298
+ "#{var}=#{@user.instance_variable_get(var)}"
299
+ end
300
+
301
+ @debug_string = @debug_values.join(", ")
302
+ @debug_string.include?("bcrypt")
303
+ #=> false
304
+
305
+ ## Debug output contains concealed markers
306
+ @debug_string.include?("[CONCEALED]")
307
+ #=> true
308
+
309
+ # Teardown
310
+ Familia.dbclient.flushdb
@@ -0,0 +1,199 @@
1
+ # try/features/encryption_fields/thread_safety_try.rb
2
+
3
+ require 'concurrent'
4
+ require 'base64'
5
+
6
+ require_relative '../../helpers/test_helpers'
7
+
8
+ # Setup encryption keys for testing
9
+ test_keys = {
10
+ v1: Base64.strict_encode64('a' * 32),
11
+ v2: Base64.strict_encode64('b' * 32)
12
+ }
13
+ Familia.config.encryption_keys = test_keys
14
+ Familia.config.current_key_version = :v1
15
+
16
+ class ThreadTest < Familia::Horreum
17
+ feature :encrypted_fields
18
+ identifier_field :id
19
+ field :id
20
+ encrypted_field :secret
21
+ end
22
+
23
+ # Thread-safe debug logging helper
24
+ @debug_mutex = Mutex.new
25
+ def debug(msg)
26
+ return unless ENV['FAMILIA_DEBUG']
27
+ @debug_mutex.synchronize { puts "DEBUG: #{msg}" }
28
+ end
29
+
30
+ ## Concurrent encryption operations maintain counter integrity
31
+ Familia::Encryption.reset_derivation_count!
32
+ @results = Concurrent::Array.new
33
+ @errors = Concurrent::Array.new
34
+
35
+ debug "Starting 10 threads for concurrent operations..."
36
+
37
+ @threads = 10.times.map do |i|
38
+ Thread.new do
39
+ begin
40
+ model = ThreadTest.new(id: "thread-#{i}")
41
+ 5.times do |j|
42
+ model.secret = "secret-#{i}-#{j}" # encrypt (derivation)
43
+ retrieved = model.secret # decrypt (derivation)
44
+ @results << retrieved
45
+ end
46
+ debug "Thread #{i} completed successfully"
47
+ rescue => e
48
+ debug "Thread #{i} failed: #{e.class}: #{e.message}"
49
+ @errors << e
50
+ end
51
+ end
52
+ end
53
+
54
+ @threads.each(&:join)
55
+
56
+ debug "All threads joined. Results: #{@results.size}, Errors: #{@errors.size}"
57
+ debug "Derivation count: #{Familia::Encryption.derivation_count.value}"
58
+
59
+ if @errors.any?
60
+ debug "Error details:"
61
+ @errors.each_with_index { |e, i| debug " #{i+1}. #{e.class}: #{e.message}" }
62
+ end
63
+
64
+ @errors.empty?
65
+ #=> true
66
+
67
+ ## All expected results collected (10 threads × 5 operations = 50 results)
68
+ @results.size
69
+ #=> 50
70
+
71
+ ## Each thread did 5 write operations = 5 derivations
72
+ # Total: 10 threads * 5 derivations = 50 (reading returns ConcealedString without decryption)
73
+ Familia::Encryption.derivation_count.value
74
+ #=> 50
75
+
76
+ ## Key rotation operations work safely under concurrent access
77
+ debug "Starting key rotation test with 4 threads..."
78
+
79
+ @rotation_errors = Concurrent::Array.new
80
+ @rotation_results = Concurrent::Array.new
81
+
82
+ @rotation_threads = 4.times.map do |i|
83
+ Thread.new do
84
+ begin
85
+ # Each thread alternates between v1 and v2
86
+ thread_version = i.even? ? :v1 : :v2
87
+
88
+ 10.times do |j|
89
+ debug "Thread #{i}, iteration #{j}, using version #{thread_version}"
90
+
91
+ # Temporarily switch key version for this operation
92
+ original_version = Familia.config.current_key_version
93
+ Familia.config.current_key_version = thread_version
94
+
95
+ begin
96
+ model = ThreadTest.new(id: "race-#{i}-#{j}")
97
+ model.secret = "test-#{i}-#{j}" # encrypt
98
+ retrieved = model.secret # decrypt
99
+ @rotation_results << retrieved
100
+ debug "Thread #{i}, iteration #{j} completed"
101
+ ensure
102
+ # Restore original version
103
+ Familia.config.current_key_version = original_version
104
+ end
105
+ end
106
+ rescue => e
107
+ debug "Rotation thread #{i} failed: #{e.class}: #{e.message}"
108
+ @rotation_errors << e
109
+ end
110
+ end
111
+ end
112
+
113
+ @rotation_threads.each(&:join)
114
+
115
+ debug "Key rotation test completed. Results: #{@rotation_results.size}, Errors: #{@rotation_errors.size}"
116
+
117
+ if @rotation_errors.any?
118
+ debug "Rotation error details:"
119
+ @rotation_errors.each_with_index { |e, i| debug " #{i+1}. #{e.class}: #{e.message}" }
120
+ end
121
+
122
+ @rotation_errors.empty?
123
+ #=> true
124
+
125
+ ## All rotation operations completed successfully (4 threads × 10 operations = 40 results)
126
+ @rotation_results.size
127
+ #=> 40
128
+
129
+ ## Atomic counter maintains accuracy under maximum contention
130
+ debug "Starting atomic counter test with 20 threads..."
131
+ debug "Count before reset: #{Familia::Encryption.derivation_count.value}"
132
+
133
+ Familia::Encryption.reset_derivation_count!
134
+ sleep(0.01) # Minimal delay to ensure reset takes effect
135
+
136
+ debug "Count after reset: #{Familia::Encryption.derivation_count.value}"
137
+
138
+ barrier = Concurrent::CyclicBarrier.new(20)
139
+ @counter_errors = Concurrent::Array.new
140
+
141
+ counter_threads = 20.times.map do |i|
142
+ Thread.new do
143
+ begin
144
+ barrier.wait # Synchronize start for maximum contention
145
+ model = ThreadTest.new(id: "counter-test-#{i}")
146
+ model.secret = 'test' # Single encrypt operation (1 derivation)
147
+ debug "Counter thread #{i} completed"
148
+ rescue => e
149
+ debug "Counter thread #{i} failed: #{e.class}: #{e.message}"
150
+ @counter_errors << e
151
+ end
152
+ end
153
+ end
154
+
155
+ counter_threads.each(&:join)
156
+
157
+ debug "Atomic counter test completed. Final count: #{Familia::Encryption.derivation_count.value}"
158
+
159
+ @counter_errors.empty?
160
+ #=> true
161
+
162
+ ## Exactly 20 derivations, no lost increments
163
+ Familia::Encryption.derivation_count.value
164
+ #=> 20
165
+
166
+ ## Concurrent encryption operations maintain counter integrity
167
+ class ThreadTest2 < Familia::Horreum
168
+ feature :encrypted_fields
169
+ identifier_field :id
170
+ field :id
171
+ encrypted_field :secret
172
+ end
173
+
174
+ Familia::Encryption.reset_derivation_count!
175
+ errors = Concurrent::Array.new
176
+ barrier = Concurrent::CyclicBarrier.new(10)
177
+
178
+ threads = 10.times.map do |i|
179
+ Thread.new do
180
+ barrier.wait # Synchronize start
181
+ model = ThreadTest.new(id: "thread-#{i}")
182
+ begin
183
+ 5.times { |j|
184
+ model.secret = "value-#{i}-#{j}" # encrypt (derivation)
185
+ model.secret # returns ConcealedString (no derivation)
186
+ }
187
+ rescue => e
188
+ errors << e
189
+ end
190
+ end
191
+ end
192
+
193
+ threads.each(&:join)
194
+ errors.empty? && Familia::Encryption.derivation_count.value == 50
195
+ #=> true
196
+
197
+ # Cleanup
198
+ Familia.config.encryption_keys = nil
199
+ Familia.config.current_key_version = nil
@@ -0,0 +1,174 @@
1
+ # try/features/encryption_fields/universal_serialization_safety_try.rb
2
+
3
+ require_relative '../../helpers/test_helpers'
4
+ require 'base64'
5
+
6
+ Familia.debug = false
7
+
8
+ # Configure encryption keys
9
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
10
+ Familia.config.encryption_keys = test_keys
11
+ Familia.config.current_key_version = :v1
12
+
13
+ # Test class with mixed field types
14
+ class DataRecord < Familia::Horreum
15
+ feature :encrypted_fields
16
+ identifier_field :id
17
+ field :id
18
+ field :title # Public field
19
+ field :description # Public field
20
+ encrypted_field :api_token # Encrypted field
21
+ encrypted_field :secret_notes # Encrypted field
22
+ encrypted_field :user_data # Encrypted field
23
+ end
24
+
25
+ # Clean environment
26
+ Familia.dbclient.flushdb
27
+
28
+ # Create test record with mixed data
29
+ @record = DataRecord.new
30
+ @record.id = "rec001"
31
+ @record.title = "Public Record"
32
+ @record.description = "This is public information"
33
+ @record.api_token = "token-abc123456789"
34
+ @record.secret_notes = "confidential information"
35
+ @record.user_data = "sensitive user details"
36
+
37
+ ## Object-level to_h excludes encrypted fields
38
+ hash_result = @record.to_h
39
+ hash_result.keys.include?("api_token")
40
+ #=> false
41
+
42
+ ## to_h excludes secret notes
43
+ @record.to_h.keys.include?("secret_notes")
44
+ #=> false
45
+
46
+ ## to_h excludes user data
47
+ @record.to_h.keys.include?("user_data")
48
+ #=> false
49
+
50
+ ## to_h only includes public fields
51
+ @record.to_h.keys.sort
52
+ #=> ["description", "id", "title"]
53
+
54
+ ## to_h contains correct public values
55
+ @record.to_h["title"]
56
+ #=> "Public Record"
57
+
58
+ ## Individual encrypted field serialization safety - to_s
59
+ @record.api_token.to_s
60
+ #=> "[CONCEALED]"
61
+
62
+ ## to_str serialization
63
+ @record.api_token.to_str
64
+ #=!> NoMethodError
65
+
66
+ ## inspect serialization
67
+ @record.api_token.inspect
68
+ #=> "[CONCEALED]"
69
+
70
+ ## JSON serialization - to_json
71
+ @record.api_token.to_json
72
+ #=> "\"[CONCEALED]\""
73
+
74
+ ## JSON serialization - as_json
75
+ @record.api_token.as_json
76
+ #=> "[CONCEALED]"
77
+
78
+ ## Hash serialization
79
+ @record.api_token.to_h
80
+ #=> "[CONCEALED]"
81
+
82
+ ## Array serialization
83
+ @record.api_token.to_a
84
+ #=> ["[CONCEALED]"]
85
+
86
+ ## Numeric serialization - to_i
87
+ @record.api_token.to_i
88
+ #=!> NoMethodError
89
+
90
+ ## Float serialization
91
+ @record.api_token.to_f
92
+ #=!> NoMethodError
93
+
94
+ ## Complex nested JSON structure
95
+ @nested_data = {
96
+ record: @record,
97
+ fields: {
98
+ public: [@record.title, @record.description],
99
+ encrypted: [@record.api_token, @record.secret_notes]
100
+ }
101
+ }
102
+
103
+ @serialized = @nested_data.to_json
104
+ @serialized.include?("token-abc123456789")
105
+ #=> false
106
+
107
+ ## Nested JSON contains concealed markers
108
+ @nested_data.to_json.include?("[CONCEALED]")
109
+ #=> true
110
+
111
+ ## Array of mixed field types safety
112
+ @mixed_array = [
113
+ @record.title,
114
+ @record.api_token,
115
+ @record.description,
116
+ @record.secret_notes
117
+ ]
118
+
119
+ @mixed_array.to_json.include?("token-abc123456789")
120
+ #=> false
121
+
122
+ ## Mixed array preserves public data
123
+ @mixed_array.to_json.include?("Public Record")
124
+ #=> true
125
+
126
+ ## String interpolation safety
127
+ @debug_message = "Record #{@record.id}: token=#{@record.api_token}"
128
+ @debug_message.include?("token-abc123456789")
129
+ #=> false
130
+
131
+ ## Interpolation shows concealed values
132
+ @debug_message.include?("[CONCEALED]")
133
+ #=> true
134
+
135
+ ## Hash merge operations safety
136
+ merged_hash = @record.to_h.merge({
137
+ runtime_token: @record.api_token
138
+ })
139
+
140
+ merged_hash.values.any? { |v| v.to_s.include?("token-abc123456789") }
141
+ #=> false
142
+
143
+ ## Database persistence maintains safety
144
+ @record.save
145
+ #=> true
146
+
147
+ ## Fresh load serialization safety
148
+ @fresh_record = DataRecord.load("rec001")
149
+ @fresh_record.to_h.keys.include?("api_token")
150
+ #=> false
151
+
152
+ ## Fresh record field safety
153
+ @fresh_record.api_token.to_s
154
+ #=> "[CONCEALED]"
155
+
156
+ ## Exception handling safety
157
+ begin
158
+ raise StandardError, "Auth failed: #{@record.api_token}"
159
+ rescue StandardError => e
160
+ e.message.include?("token-abc123456789")
161
+ end
162
+ #=> false
163
+
164
+ ## String formatting safety
165
+ @formatted = "Token: %s" % [@record.api_token]
166
+ @formatted.include?("token-abc123456789")
167
+ #=> false
168
+
169
+ ## Formatted string shows concealed
170
+ @formatted.include?("[CONCEALED]")
171
+ #=> true
172
+
173
+ # Teardown
174
+ Familia.dbclient.flushdb
@@ -1,6 +1,5 @@
1
1
  # try/features/expiration_try.rb
2
2
 
3
- require_relative '../../lib/familia'
4
3
  require_relative '../helpers/test_helpers'
5
4
 
6
5
  Familia.debug = false