familia 2.0.0.pre3 → 2.0.0.pre5
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 +3 -3
- data/Gemfile +5 -1
- data/Gemfile.lock +18 -3
- data/README.md +36 -157
- data/TEST_COVERAGE.md +40 -0
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +270 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
- data/docs/wiki/Home.md +49 -0
- data/docs/wiki/Implementation-Guide.md +183 -0
- data/docs/wiki/Security-Model.md +143 -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/{datatype → data_type}/types/hashkey.rb +2 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +10 -12
- data/lib/familia/encryption/manager.rb +102 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -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/features/encrypted_fields/encrypted_field_type.rb +153 -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 +270 -0
- data/lib/familia/horreum/connection.rb +8 -11
- data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
- data/lib/familia/horreum/definition_methods.rb +453 -0
- data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
- data/lib/familia/horreum/serialization.rb +46 -18
- data/lib/familia/horreum/settings.rb +10 -2
- data/lib/familia/horreum/utils.rb +9 -10
- data/lib/familia/horreum.rb +18 -10
- 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 +2 -1
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/errors_try.rb +0 -1
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +0 -1
- 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/{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/{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/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 +117 -0
- data/try/features/encrypted_fields_integration_try.rb +220 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
- data/try/features/encrypted_fields_security_try.rb +370 -0
- data/try/features/encryption_fields/aad_protection_try.rb +53 -0
- data/try/features/encryption_fields/context_isolation_try.rb +120 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
- data/try/features/encryption_fields/fresh_key_try.rb +163 -0
- data/try/features/encryption_fields/key_rotation_try.rb +117 -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 +54 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -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 +42 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/class_methods_try.rb +27 -36
- 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 +0 -1
- data/try/horreum/relations_try.rb +0 -1
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +2 -3
- 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 +0 -1
- data/try/models/customer_try.rb +0 -1
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +85 -18
@@ -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?("Decryption failed")
|
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?("Decryption failed")
|
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?("Decryption failed")
|
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]
|