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,328 @@
1
+ # try/encryption/encryption_core_try.rb
2
+
3
+ require_relative '../helpers/test_helpers'
4
+ require_relative '../../lib/familia/encryption'
5
+ require 'base64'
6
+
7
+ # Test constants will be redefined in each test since variables don't persist
8
+
9
+ ## Basic round-trip encryption and decryption works
10
+ test_keys = { v1: Base64.strict_encode64('a' * 32), v2: Base64.strict_encode64('b' * 32) }
11
+ context = "TestModel:secret_field:user123"
12
+ plaintext = "sensitive data here"
13
+
14
+ Familia.config.encryption_keys = test_keys
15
+ Familia.config.current_key_version = :v2
16
+ encrypted = Familia::Encryption.encrypt(plaintext, context: context)
17
+ decrypted = Familia::Encryption.decrypt(encrypted, context: context)
18
+ decrypted
19
+ #=> "sensitive data here"
20
+
21
+ ## Encrypted data contains expected JSON structure
22
+ test_keys = { v1: Base64.strict_encode64('a' * 32), v2: Base64.strict_encode64('b' * 32) }
23
+ context = "TestModel:secret_field:user123"
24
+ plaintext = "sensitive data here"
25
+
26
+ Familia.config.encryption_keys = test_keys
27
+ Familia.config.current_key_version = :v2
28
+ encrypted = Familia::Encryption.encrypt(plaintext, context: context)
29
+ encrypted_data = JSON.parse(encrypted, symbolize_names: true)
30
+ encrypted_data[:algorithm]
31
+ #=> "xchacha20poly1305"
32
+
33
+ ## Encrypted data includes current key version
34
+ test_keys = { v1: Base64.strict_encode64('a' * 32), v2: Base64.strict_encode64('b' * 32) }
35
+ context = "TestModel:secret_field:user123"
36
+ plaintext = "sensitive data here"
37
+
38
+ Familia.config.encryption_keys = test_keys
39
+ Familia.config.current_key_version = :v2
40
+ encrypted = Familia::Encryption.encrypt(plaintext, context: context)
41
+ encrypted_data = JSON.parse(encrypted, symbolize_names: true)
42
+ encrypted_data[:key_version]
43
+ #=> "v2"## Nonce is unique - same plaintext encrypts to different ciphertext
44
+ test_keys = { v1: Base64.strict_encode64('a' * 32), v2: Base64.strict_encode64('b' * 32) }
45
+ context = "TestModel:secret_field:user123"
46
+ plaintext = "sensitive data here"
47
+
48
+ Familia.config.encryption_keys = test_keys
49
+ Familia.config.current_key_version = :v2
50
+ encrypted1 = Familia::Encryption.encrypt(plaintext, context: context)
51
+ encrypted2 = Familia::Encryption.encrypt(plaintext, context: context)
52
+ encrypted1 != encrypted2
53
+ #=> true
54
+
55
+ ## But both decrypt to same plaintext
56
+ test_keys = { v1: Base64.strict_encode64('a' * 32), v2: Base64.strict_encode64('b' * 32) }
57
+ context = "TestModel:secret_field:user123"
58
+ plaintext = "sensitive data here"
59
+
60
+ Familia.config.encryption_keys = test_keys
61
+ Familia.config.current_key_version = :v2
62
+ encrypted1 = Familia::Encryption.encrypt(plaintext, context: context)
63
+ encrypted2 = Familia::Encryption.encrypt(plaintext, context: context)
64
+ decrypted1 = Familia::Encryption.decrypt(encrypted1, context: context)
65
+ decrypted2 = Familia::Encryption.decrypt(encrypted2, context: context)
66
+ [decrypted1, decrypted2]
67
+ #=> ["sensitive data here", "sensitive data here"]
68
+
69
+ ## Nil plaintext returns nil
70
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
71
+ Familia.config.encryption_keys = test_keys
72
+ Familia.config.current_key_version = :v1
73
+ Familia::Encryption.encrypt(nil, context: 'test')
74
+ #=> nil
75
+
76
+ ## Empty string plaintext returns nil
77
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
78
+ Familia.config.encryption_keys = test_keys
79
+ Familia.config.current_key_version = :v1
80
+ Familia::Encryption.encrypt("", context: 'test')
81
+ #=> nil
82
+
83
+ ## AAD prevents decryption with wrong additional data - raises EncryptionError
84
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
85
+ context = "TestModel:secret_field:user123"
86
+ plaintext = "sensitive data here"
87
+ additional_data = "user123:email@example.com"
88
+
89
+ Familia.config.encryption_keys = test_keys
90
+ Familia.config.current_key_version = :v1
91
+ encrypted = Familia::Encryption.encrypt(plaintext, context: context, additional_data: additional_data)
92
+
93
+ begin
94
+ Familia::Encryption.decrypt(encrypted, context: context, additional_data: "wrong_aad")
95
+ "should_not_reach_here"
96
+ rescue Familia::EncryptionError => e
97
+ e.message.include?("Decryption failed")
98
+ end
99
+ #=> true
100
+
101
+
102
+ ## Unknown algorithm raises sanitized error
103
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
104
+ context = "TestModel:secret_field:user123"
105
+
106
+ Familia.config.encryption_keys = test_keys
107
+ Familia.config.current_key_version = :v1
108
+
109
+ # Create encrypted data with unknown algorithm. Error should not leak algorithm details.
110
+ invalid_encrypted = {
111
+ algorithm: "unknown-cipher",
112
+ key_version: "v1",
113
+ nonce: Base64.strict_encode64("x" * 12),
114
+ ciphertext: Base64.strict_encode64("encrypted_data"),
115
+ auth_tag: Base64.strict_encode64("y" * 16)
116
+ }.to_json
117
+
118
+ Familia::Encryption.decrypt(invalid_encrypted, context: context)
119
+ #=!> Familia::EncryptionError
120
+ #==> error.message.include?("Unsupported algorithm")
121
+ #==> error.message.include?("unknown-cipher")
122
+
123
+ ## Malformed JSON raises sanitized error
124
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
125
+ context = "TestModel:secret_field:user123"
126
+
127
+ Familia.config.encryption_keys = test_keys
128
+ Familia.config.current_key_version = :v1
129
+
130
+ Familia::Encryption.decrypt("invalid json {", context: context)
131
+ #=!> Familia::EncryptionError
132
+ #==> error.message.include?("Invalid JSON structure")
133
+
134
+ ## Invalid base64 nonce raises sanitized error
135
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
136
+ context = "TestModel:secret_field:user123"
137
+
138
+ Familia.config.encryption_keys = test_keys
139
+ Familia.config.current_key_version = :v1
140
+
141
+ # Create encrypted data with invalid base64 nonce
142
+ invalid_encrypted = {
143
+ algorithm: "aes-256-gcm",
144
+ key_version: "v1",
145
+ nonce: "invalid_base64!@#",
146
+ ciphertext: Base64.strict_encode64("encrypted_data"),
147
+ auth_tag: Base64.strict_encode64("y" * 16)
148
+ }.to_json
149
+
150
+ begin
151
+ Familia::Encryption.decrypt(invalid_encrypted, context: context)
152
+ "should_not_reach_here"
153
+ rescue Familia::EncryptionError => e
154
+ e.message.include?("Invalid Base64 encoding")
155
+ end
156
+ #=> true
157
+
158
+ ## Invalid base64 auth_tag raises sanitized error
159
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
160
+ context = "TestModel:secret_field:user123"
161
+
162
+ Familia.config.encryption_keys = test_keys
163
+ Familia.config.current_key_version = :v1
164
+
165
+ # Create encrypted data with invalid base64 auth_tag
166
+ invalid_encrypted = {
167
+ algorithm: "aes-256-gcm",
168
+ key_version: "v1",
169
+ nonce: Base64.strict_encode64("x" * 12),
170
+ ciphertext: Base64.strict_encode64("encrypted_data"),
171
+ auth_tag: "invalid_base64!@#"
172
+ }.to_json
173
+
174
+ begin
175
+ Familia::Encryption.decrypt(invalid_encrypted, context: context)
176
+ "should_not_reach_here"
177
+ rescue Familia::EncryptionError => e
178
+ e.message.include?("Invalid Base64 encoding")
179
+ end
180
+ #=> true
181
+
182
+ ## Wrong nonce size raises sanitized error
183
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
184
+ context = "TestModel:secret_field:user123"
185
+
186
+ Familia.config.encryption_keys = test_keys
187
+ Familia.config.current_key_version = :v1
188
+
189
+ # Create encrypted data with wrong nonce size (8 bytes instead of 12)
190
+ invalid_encrypted = {
191
+ algorithm: "aes-256-gcm",
192
+ key_version: "v1",
193
+ nonce: Base64.strict_encode64("x" * 8),
194
+ ciphertext: Base64.strict_encode64("encrypted_data"),
195
+ auth_tag: Base64.strict_encode64("y" * 16)
196
+ }.to_json
197
+
198
+ begin
199
+ Familia::Encryption.decrypt(invalid_encrypted, context: context)
200
+ "should_not_reach_here"
201
+ rescue Familia::EncryptionError => e
202
+ e.message.include?("Invalid encrypted data")
203
+ end
204
+ #=> true
205
+
206
+ ## Wrong auth_tag size raises sanitized error
207
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
208
+ context = "TestModel:secret_field:user123"
209
+
210
+ Familia.config.encryption_keys = test_keys
211
+ Familia.config.current_key_version = :v1
212
+
213
+ # Create encrypted data with wrong auth_tag size (8 bytes instead of 16)
214
+ invalid_encrypted = {
215
+ algorithm: "aes-256-gcm",
216
+ key_version: "v1",
217
+ nonce: Base64.strict_encode64("x" * 12),
218
+ ciphertext: Base64.strict_encode64("encrypted_data"),
219
+ auth_tag: Base64.strict_encode64("y" * 8)
220
+ }.to_json
221
+
222
+ begin
223
+ Familia::Encryption.decrypt(invalid_encrypted, context: context)
224
+ "should_not_reach_here"
225
+ rescue Familia::EncryptionError => e
226
+ e.message.include?("Invalid encrypted data")
227
+ end
228
+ #=> true
229
+
230
+ ## Missing required fields raises sanitized error
231
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
232
+ context = "TestModel:secret_field:user123"
233
+
234
+ Familia.config.encryption_keys = test_keys
235
+ Familia.config.current_key_version = :v1
236
+
237
+ # Create encrypted data missing nonce field
238
+ invalid_encrypted = {
239
+ algorithm: "aes-256-gcm",
240
+ key_version: "v1",
241
+ ciphertext: Base64.strict_encode64("encrypted_data"),
242
+ auth_tag: Base64.strict_encode64("y" * 16)
243
+ }.to_json
244
+
245
+ begin
246
+ Familia::Encryption.decrypt(invalid_encrypted, context: context)
247
+ "should_not_reach_here"
248
+ rescue Familia::EncryptionError => e
249
+ e.message.include?("Decryption failed")
250
+ end
251
+ #=> true
252
+
253
+ ## Algorithm-specific encryption: XChaCha20Poly1305 round-trip
254
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
255
+ context = "TestModel:secret_field:user123"
256
+ plaintext = "xchacha20poly1305 test data"
257
+
258
+ Familia.config.encryption_keys = test_keys
259
+ Familia.config.current_key_version = :v1
260
+ encrypted_xchacha = Familia::Encryption.encrypt_with('xchacha20poly1305', plaintext, context: context)
261
+ decrypted_xchacha = Familia::Encryption.decrypt(encrypted_xchacha, context: context)
262
+ decrypted_xchacha
263
+ #=> "xchacha20poly1305 test data"
264
+
265
+ ## Algorithm-specific encryption: AES-GCM round-trip
266
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
267
+ context = "TestModel:secret_field:user123"
268
+ plaintext = "aes-256-gcm test data"
269
+
270
+ Familia.config.encryption_keys = test_keys
271
+ Familia.config.current_key_version = :v1
272
+ encrypted_aes = Familia::Encryption.encrypt_with('aes-256-gcm', plaintext, context: context)
273
+ decrypted_aes = Familia::Encryption.decrypt(encrypted_aes, context: context)
274
+ decrypted_aes
275
+ #=> "aes-256-gcm test data"
276
+
277
+ ## XChaCha20Poly1305 has correct algorithm in encrypted data
278
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
279
+ context = "TestModel:secret_field:user123"
280
+ plaintext = "algorithm check"
281
+
282
+ Familia.config.encryption_keys = test_keys
283
+ Familia.config.current_key_version = :v1
284
+ encrypted_xchacha = Familia::Encryption.encrypt_with('xchacha20poly1305', plaintext, context: context)
285
+ encrypted_data_xchacha = JSON.parse(encrypted_xchacha, symbolize_names: true)
286
+ encrypted_data_xchacha[:algorithm]
287
+ #=> "xchacha20poly1305"
288
+
289
+ ## AES-GCM has correct algorithm in encrypted data
290
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
291
+ context = "TestModel:secret_field:user123"
292
+ plaintext = "algorithm check"
293
+
294
+ Familia.config.encryption_keys = test_keys
295
+ Familia.config.current_key_version = :v1
296
+ encrypted_aes = Familia::Encryption.encrypt_with('aes-256-gcm', plaintext, context: context)
297
+ encrypted_data_aes = JSON.parse(encrypted_aes, symbolize_names: true)
298
+ encrypted_data_aes[:algorithm]
299
+ #=> "aes-256-gcm"
300
+
301
+ ## XChaCha20Poly1305 uses 24-byte nonces
302
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
303
+ context = "TestModel:secret_field:user123"
304
+ plaintext = "nonce size test"
305
+
306
+ Familia.config.encryption_keys = test_keys
307
+ Familia.config.current_key_version = :v1
308
+ encrypted_xchacha = Familia::Encryption.encrypt_with('xchacha20poly1305', plaintext, context: context)
309
+ encrypted_data_xchacha = JSON.parse(encrypted_xchacha, symbolize_names: true)
310
+ nonce_bytes_xchacha = Base64.strict_decode64(encrypted_data_xchacha[:nonce])
311
+ nonce_bytes_xchacha.length
312
+ #=> 24
313
+
314
+ ## AES-GCM uses 12-byte nonces
315
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
316
+ context = "TestModel:secret_field:user123"
317
+ plaintext = "nonce size test"
318
+
319
+ Familia.config.encryption_keys = test_keys
320
+ Familia.config.current_key_version = :v1
321
+ encrypted_aes = Familia::Encryption.encrypt_with('aes-256-gcm', plaintext, context: context)
322
+ encrypted_data_aes = JSON.parse(encrypted_aes, symbolize_names: true)
323
+ nonce_bytes_aes = Base64.strict_decode64(encrypted_data_aes[:nonce])
324
+ nonce_bytes_aes.length
325
+ #=> 12
326
+
327
+ # TEARDOWN
328
+ Thread.current[:familia_key_cache]&.clear if Thread.current[:familia_key_cache]
@@ -0,0 +1,31 @@
1
+ # try/encryption/debug3_try.rb
2
+
3
+ # - Tests instance variable scoping in tryouts framework
4
+ # - Validates that variables persist within test sections
5
+
6
+
7
+ require 'base64'
8
+
9
+ require_relative '../helpers/test_helpers'
10
+
11
+ @test_keys = {
12
+ v1: Base64.strict_encode64('a' * 32)
13
+ }
14
+
15
+ ## Check if instance variables work
16
+ @test_keys.nil?
17
+ #=> false
18
+
19
+ ## Check if we can access specific key
20
+ @test_keys[:v1].nil?
21
+ #=> false
22
+
23
+ ## Set config and check immediately in same test
24
+ Familia.config.encryption_keys = @test_keys
25
+ Familia.config.current_key_version = :v1
26
+ result = Familia::Encryption.encrypt('test', context: 'test')
27
+ result.nil?
28
+ #=> false
29
+
30
+
31
+ # TEARDOWN
@@ -0,0 +1,28 @@
1
+ # try/encryption/debug_try.rb
2
+
3
+ # - Tests that the encryption module loads correctly
4
+ # - Validates basic configuration setup
5
+
6
+ require 'base64'
7
+
8
+ require_relative '../helpers/test_helpers'
9
+
10
+ # Test basic functionality
11
+
12
+ ## Check if encryption module loads
13
+ defined?(Familia::Encryption)
14
+ #=> "constant"
15
+
16
+ ## Set and check configuration directly in test
17
+ Familia.encryption_keys = { v1: Base64.strict_encode64('a' * 32) }
18
+ Familia.encryption_keys.is_a?(Hash)
19
+ #=> true
20
+
21
+ ## Set and check current key version directly in test
22
+ Familia.current_key_version = :v1
23
+ Familia.current_key_version
24
+ #=> :v1
25
+
26
+
27
+ # TEARDOWN
28
+ # Clean up
@@ -0,0 +1,178 @@
1
+ # try/encryption/providers/aes_gcm_provider_try.rb
2
+
3
+ require_relative '../../helpers/test_helpers'
4
+ require 'base64'
5
+
6
+ ## AES-GCM provider availability check (always available with OpenSSL)
7
+ Familia::Encryption::Providers::AESGCMProvider.available?
8
+ #=> true
9
+
10
+ ## AES-GCM provider initialization
11
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
12
+ [provider.algorithm, provider.nonce_size, provider.auth_tag_size]
13
+ #=> ['aes-256-gcm', 12, 16]
14
+
15
+ ## AES-GCM provider priority is fallback
16
+ Familia::Encryption::Providers::AESGCMProvider.priority
17
+ #=> 50
18
+
19
+ ## AES-GCM nonce generation produces correct size
20
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
21
+ nonce = provider.generate_nonce
22
+ nonce.bytesize
23
+ #=> 12
24
+
25
+ ## AES-GCM key derivation with HKDF
26
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
27
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
28
+ master_key = Base64.strict_decode64(test_keys[:v1])
29
+ context = 'test-context'
30
+ derived_key = provider.derive_key(master_key, context)
31
+ derived_key.bytesize
32
+ #=> 32
33
+
34
+ ## AES-GCM key derivation produces different keys for different contexts
35
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
36
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
37
+ master_key = Base64.strict_decode64(test_keys[:v1])
38
+ derived_key1 = provider.derive_key(master_key, 'context1')
39
+ derived_key2 = provider.derive_key(master_key, 'context2')
40
+ derived_key1 != derived_key2
41
+ #=> true
42
+
43
+ ## AES-GCM encryption produces expected structure
44
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
45
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
46
+ key = Base64.strict_decode64(test_keys[:v1])
47
+ plaintext = 'test encryption data'
48
+ result = provider.encrypt(plaintext, key)
49
+ [result.has_key?(:nonce), result.has_key?(:ciphertext), result.has_key?(:auth_tag)]
50
+ #=> [true, true, true]
51
+
52
+ ## AES-GCM encryption nonce is 12 bytes
53
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
54
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
55
+ key = Base64.strict_decode64(test_keys[:v1])
56
+ plaintext = 'test encryption data'
57
+ result = provider.encrypt(plaintext, key)
58
+ result[:nonce].bytesize
59
+ #=> 12
60
+
61
+ ## AES-GCM encryption auth tag is 16 bytes
62
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
63
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
64
+ key = Base64.strict_decode64(test_keys[:v1])
65
+ plaintext = 'test encryption data'
66
+ result = provider.encrypt(plaintext, key)
67
+ result[:auth_tag].bytesize
68
+ #=> 16
69
+
70
+ ## AES-GCM round-trip encryption/decryption
71
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
72
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
73
+ key = Base64.strict_decode64(test_keys[:v1])
74
+ plaintext = 'AES-GCM round-trip test'
75
+ encrypted = provider.encrypt(plaintext, key)
76
+ decrypted = provider.decrypt(encrypted[:ciphertext], key, encrypted[:nonce], encrypted[:auth_tag])
77
+ decrypted
78
+ #=> 'AES-GCM round-trip test'
79
+
80
+ ## AES-GCM encryption with AAD (Additional Authenticated Data)
81
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
82
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
83
+ key = Base64.strict_decode64(test_keys[:v1])
84
+ plaintext = 'test with aad'
85
+ aad = 'additional-authenticated-data'
86
+ encrypted = provider.encrypt(plaintext, key, aad)
87
+ decrypted = provider.decrypt(encrypted[:ciphertext], key, encrypted[:nonce], encrypted[:auth_tag], aad)
88
+ decrypted
89
+ #=> 'test with aad'
90
+
91
+ ## AES-GCM AAD tampering fails authentication
92
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
93
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
94
+ key = Base64.strict_decode64(test_keys[:v1])
95
+ plaintext = 'test with aad'
96
+ aad = 'original-aad'
97
+ encrypted = provider.encrypt(plaintext, key, aad)
98
+ provider.decrypt(encrypted[:ciphertext], key, encrypted[:nonce], encrypted[:auth_tag], 'tampered-aad')
99
+ #=!> Familia::EncryptionError
100
+ #==> error.message.include?('Decryption failed')
101
+
102
+ ## AES-GCM nonce tampering fails authentication
103
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
104
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
105
+ key = Base64.strict_decode64(test_keys[:v1])
106
+ plaintext = 'test nonce tampering'
107
+ encrypted = provider.encrypt(plaintext, key)
108
+ tampered_nonce = 'x' * 12 # Wrong nonce (12 bytes for AES-GCM)
109
+ provider.decrypt(encrypted[:ciphertext], key, tampered_nonce, encrypted[:auth_tag])
110
+ #=!> Familia::EncryptionError
111
+ #==> error.message.include?('Decryption failed')
112
+
113
+ ## AES-GCM auth tag tampering fails authentication
114
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
115
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
116
+ key = Base64.strict_decode64(test_keys[:v1])
117
+ plaintext = 'test auth tag tampering'
118
+ encrypted = provider.encrypt(plaintext, key)
119
+ tampered_auth_tag = 'y' * 16 # Wrong auth tag
120
+ provider.decrypt(encrypted[:ciphertext], key, encrypted[:nonce], tampered_auth_tag)
121
+ #=!> Familia::EncryptionError
122
+ #==> error.message.include?('Decryption failed')
123
+
124
+ ## AES-GCM ciphertext tampering fails authentication
125
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
126
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
127
+ key = Base64.strict_decode64(test_keys[:v1])
128
+ plaintext = 'test ciphertext tampering'
129
+ encrypted = provider.encrypt(plaintext, key)
130
+ tampered_ciphertext = encrypted[:ciphertext][0..-2] + 'X' # Change last byte
131
+ provider.decrypt(tampered_ciphertext, key, encrypted[:nonce], encrypted[:auth_tag])
132
+ #=!> Familia::EncryptionError
133
+ #==> error.message.include?('Decryption failed')
134
+
135
+ ## AES-GCM key validation rejects nil key
136
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
137
+ provider.encrypt('test', nil)
138
+ #=!> Familia::EncryptionError
139
+ #==> error.message.include?('Key cannot be nil')
140
+
141
+ ## AES-GCM key validation requires 32-byte minimum
142
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
143
+ short_key = 'x' * 16 # Only 16 bytes
144
+ provider.encrypt('test', short_key)
145
+ #=!> Familia::EncryptionError
146
+ #==> error.message.include?('Key must be at least 32 bytes')
147
+
148
+ ## AES-GCM secure_wipe clears key (best effort)
149
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
150
+ test_key = 'secret-key-data-to-be-wiped'
151
+ original_length = test_key.length
152
+ provider.secure_wipe(test_key)
153
+ test_key.length
154
+ #=> 0
155
+
156
+ ## AES-GCM derive_key method signature (no personal parameter)
157
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
158
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
159
+ master_key = Base64.strict_decode64(test_keys[:v1])
160
+ context = 'signature-test'
161
+ # AES-GCM derive_key only takes master_key and context (no personal parameter)
162
+ derived_key = provider.derive_key(master_key, context)
163
+ derived_key.bytesize
164
+ #=> 32
165
+
166
+ ## AES-GCM HKDF salt consistency
167
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
168
+ provider = Familia::Encryption::Providers::AESGCMProvider.new
169
+ master_key = Base64.strict_decode64(test_keys[:v1])
170
+ context = 'salt-test'
171
+ # Multiple derivations with same inputs should produce same key
172
+ derived_key1 = provider.derive_key(master_key, context)
173
+ derived_key2 = provider.derive_key(master_key, context)
174
+ derived_key1 == derived_key2
175
+ #=> true
176
+
177
+ # TEARDOWN
178
+ Thread.current[:familia_key_cache]&.clear if Thread.current[:familia_key_cache]