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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +11 -8
- data/Gemfile +5 -1
- data/Gemfile.lock +19 -3
- data/README.md +36 -157
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +347 -0
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +600 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +106 -0
- data/docs/wiki/Implementation-Guide.md +276 -0
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +183 -0
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +18 -27
- data/lib/familia/connection.rb +6 -5
- data/lib/familia/{datatype → data_type}/commands.rb +2 -5
- data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +12 -14
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +119 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
- data/lib/familia/encryption/registry.rb +50 -0
- data/lib/familia/encryption.rb +178 -0
- data/lib/familia/encryption_request_cache.rb +68 -0
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
- data/lib/familia/features/encrypted_fields.rb +28 -0
- data/lib/familia/features/expiration.rb +107 -77
- data/lib/familia/features/quantization.rb +5 -9
- data/lib/familia/features/relatable_objects.rb +2 -4
- data/lib/familia/features/safe_dump.rb +14 -17
- data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
- data/lib/familia/features/transient_fields.rb +47 -0
- data/lib/familia/features.rb +40 -24
- data/lib/familia/field_type.rb +273 -0
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
- data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
- data/lib/familia/horreum/subclass/definition.rb +469 -0
- data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +30 -22
- data/lib/familia/logging.rb +14 -14
- data/lib/familia/settings.rb +39 -3
- data/lib/familia/utils.rb +45 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +3 -2
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -5
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +1 -2
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/core/pools_try.rb +2 -2
- data/try/core/secure_identifier_try.rb +0 -1
- data/try/core/settings_try.rb +0 -1
- data/try/core/utils_try.rb +0 -1
- data/try/{datatypes → data_types}/boolean_try.rb +1 -2
- data/try/data_types/counter_try.rb +93 -0
- data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
- data/try/{datatypes → data_types}/hash_try.rb +1 -2
- data/try/{datatypes → data_types}/list_try.rb +1 -2
- data/try/data_types/lock_try.rb +133 -0
- data/try/{datatypes → data_types}/set_try.rb +1 -2
- data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
- data/try/{datatypes → data_types}/string_try.rb +1 -2
- data/try/debugging/README.md +32 -0
- data/try/debugging/cache_behavior_tracer.rb +91 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/debugging/encryption_method_tracer.rb +138 -0
- data/try/debugging/provider_diagnostics.rb +110 -0
- data/try/edge_cases/hash_symbolization_try.rb +0 -1
- data/try/edge_cases/json_serialization_try.rb +0 -1
- data/try/edge_cases/reserved_keywords_try.rb +42 -11
- data/try/encryption/config_persistence_try.rb +192 -0
- data/try/encryption/encryption_core_try.rb +328 -0
- data/try/encryption/instance_variable_scope_try.rb +31 -0
- data/try/encryption/module_loading_try.rb +28 -0
- data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
- data/try/encryption/roundtrip_validation_try.rb +28 -0
- data/try/encryption/secure_memory_handling_try.rb +125 -0
- data/try/features/encrypted_fields_core_try.rb +125 -0
- data/try/features/encrypted_fields_integration_try.rb +216 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
- data/try/features/encrypted_fields_security_try.rb +377 -0
- data/try/features/encryption_fields/aad_protection_try.rb +138 -0
- data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +141 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
- data/try/features/encryption_fields/fresh_key_try.rb +168 -0
- data/try/features/encryption_fields/key_rotation_try.rb +123 -0
- data/try/features/encryption_fields/memory_security_try.rb +37 -0
- data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/expiration_try.rb +0 -1
- data/try/features/feature_dependencies_try.rb +159 -0
- data/try/features/quantization_try.rb +0 -1
- data/try/features/real_feature_integration_try.rb +148 -0
- data/try/features/relatable_objects_try.rb +0 -1
- data/try/features/safe_dump_advanced_try.rb +0 -1
- data/try/features/safe_dump_try.rb +0 -1
- data/try/features/transient_fields/redacted_string_try.rb +248 -0
- data/try/features/transient_fields/refresh_reset_try.rb +164 -0
- data/try/features/transient_fields/simple_refresh_test.rb +50 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
- data/try/features/transient_fields_core_try.rb +181 -0
- data/try/features/transient_fields_integration_try.rb +260 -0
- data/try/helpers/test_helpers.rb +67 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
- data/try/horreum/field_categories_try.rb +118 -0
- data/try/horreum/field_definition_try.rb +96 -0
- data/try/horreum/initialization_try.rb +1 -2
- data/try/horreum/relations_try.rb +1 -2
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +41 -7
- data/try/memory/memory_basic_test.rb +73 -0
- data/try/memory/memory_detailed_test.rb +121 -0
- data/try/memory/memory_docker_ruby_dump.sh +80 -0
- data/try/memory/memory_search_for_string.rb +83 -0
- data/try/memory/test_actual_redactedstring_protection.rb +38 -0
- data/try/models/customer_safe_dump_try.rb +1 -2
- data/try/models/customer_try.rb +1 -2
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +131 -23
- 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
|