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,80 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
4
+ ENV['TEST'] = 'true'
5
+ require 'familia'
6
+ require_relative 'try/helpers/test_helpers'
7
+
8
+ puts "Understanding the test design and expected behavior..."
9
+
10
+ # Setup encryption keys
11
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
12
+ Familia.config.encryption_keys = test_keys
13
+ Familia.config.current_key_version = :v1
14
+
15
+ class TestModelA < Familia::Horreum
16
+ feature :encrypted_fields
17
+ identifier_field :id
18
+ field :id
19
+ encrypted_field :api_key
20
+ end
21
+
22
+ class TestModelB < Familia::Horreum
23
+ feature :encrypted_fields
24
+ identifier_field :id
25
+ field :id
26
+ encrypted_field :api_key
27
+ end
28
+
29
+ # Clean database
30
+ Familia.dbclient.flushdb
31
+
32
+ model_a = TestModelA.new(id: 'same-id')
33
+ model_b = TestModelB.new(id: 'same-id')
34
+
35
+ model_a.api_key = 'secret-key'
36
+ model_b.api_key = 'secret-key'
37
+
38
+ cipher_a = model_a.instance_variable_get(:@api_key)
39
+ cipher_b = model_b.instance_variable_get(:@api_key)
40
+
41
+ puts "cipher_a class: #{cipher_a.class}"
42
+ puts "cipher_b class: #{cipher_b.class}"
43
+
44
+ # What the current tests do:
45
+ puts "\n=== Current test approach ==="
46
+ model_a.instance_variable_set(:@api_key, cipher_b)
47
+ result = model_a.api_key
48
+ puts "After setting cipher_b into model_a:"
49
+ puts " api_key returns: #{result.class}"
50
+ puts " This should be ConcealedString and succeed"
51
+
52
+ # What would test ACTUAL cross-context isolation:
53
+ puts "\n=== Testing actual cross-context isolation ==="
54
+ puts "The REAL test should be trying to decrypt:"
55
+ begin
56
+ result.reveal do |plain|
57
+ puts " Cross-context decryption succeeded: #{plain} (BAD)"
58
+ end
59
+ rescue => e
60
+ puts " Cross-context decryption failed: #{e.class} (GOOD)"
61
+ end
62
+
63
+ # Try with raw encrypted JSON to see if that behaves differently:
64
+ puts "\n=== Testing with raw encrypted JSON ==="
65
+ raw_encrypted_b = cipher_b.encrypted_value
66
+ puts "Raw encrypted from B: #{raw_encrypted_b}"
67
+
68
+ # Try to set raw encrypted JSON and see what happens
69
+ model_a.api_key = raw_encrypted_b # This should wrap it in ConcealedString
70
+ result2 = model_a.api_key
71
+ puts "After setting raw encrypted JSON:"
72
+ puts " api_key returns: #{result2.class}"
73
+
74
+ begin
75
+ result2.reveal do |plain|
76
+ puts " Raw JSON decryption result: #{plain}"
77
+ end
78
+ rescue => e
79
+ puts " Raw JSON decryption failed: #{e.class}: #{e.message}"
80
+ end
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env ruby
2
+ # Debug script to trace encryption method calls and data flow
3
+
4
+ require 'base64'
5
+ require 'bundler/setup'
6
+ require_relative '../helpers/test_helpers'
7
+
8
+ puts "=== Encryption Method Call Tracer ==="
9
+ puts
10
+
11
+ # Monkey patch to add detailed tracing
12
+ module Familia
13
+ module Encryption
14
+ class << self
15
+ # Store original methods
16
+ alias_method :orig_encrypt, :encrypt
17
+ alias_method :orig_decrypt, :decrypt
18
+
19
+ def encrypt(plaintext, context:, additional_data: nil)
20
+ puts "📤 ENCRYPT called:"
21
+ puts " Context: #{context}"
22
+ puts " Plaintext length: #{plaintext&.length} chars"
23
+ puts " AAD: #{additional_data ? 'present' : 'none'}"
24
+
25
+ start_time = Time.now
26
+ result = orig_encrypt(plaintext, context: context, additional_data: additional_data)
27
+ elapsed = ((Time.now - start_time) * 1000).round(2)
28
+
29
+ puts " Result length: #{result ? result.length : 'nil'} chars"
30
+ puts " Elapsed: #{elapsed}ms"
31
+ puts
32
+ result
33
+ end
34
+
35
+ def decrypt(encrypted_json, context:, additional_data: nil)
36
+ puts "📥 DECRYPT called:"
37
+ puts " Context: #{context}"
38
+ puts " Encrypted length: #{encrypted_json&.length} chars"
39
+ puts " AAD: #{additional_data ? 'present' : 'none'}"
40
+
41
+ start_time = Time.now
42
+ begin
43
+ result = orig_decrypt(encrypted_json, context: context, additional_data: additional_data)
44
+ elapsed = ((Time.now - start_time) * 1000).round(2)
45
+
46
+ puts " Result length: #{result ? result.length : 'nil'} chars"
47
+ puts " Elapsed: #{elapsed}ms"
48
+ puts
49
+ result
50
+ rescue => e
51
+ elapsed = ((Time.now - start_time) * 1000).round(2)
52
+ puts " ERROR: #{e.class}: #{e.message}"
53
+ puts " Elapsed: #{elapsed}ms"
54
+ puts
55
+ raise
56
+ end
57
+ end
58
+ end
59
+
60
+ class Manager
61
+ alias_method :orig_derive_key_with_provider, :derive_key_with_provider
62
+
63
+ def derive_key_with_provider(provider, context, version: nil)
64
+ puts "🔑 DERIVE_KEY called:"
65
+ puts " Provider: #{provider.class.name}"
66
+ puts " Context: #{context}"
67
+ puts " Version: #{version || 'current'}"
68
+
69
+ cache = Thread.current[:familia_key_cache] ||= {}
70
+ cache_key = "#{version || current_key_version}:#{context}"
71
+ puts " Cache key: #{cache_key}"
72
+ puts " Cache before: #{cache.keys.inspect}"
73
+
74
+ start_time = Time.now
75
+ result = orig_derive_key_with_provider(provider, context, version: version)
76
+ elapsed = ((Time.now - start_time) * 1000).round(2)
77
+
78
+ cache_after = Thread.current[:familia_key_cache] || {}
79
+ puts " Cache after: #{cache_after.keys.inspect}"
80
+ puts " Derived key: [#{result.bytesize} bytes]"
81
+ puts " Elapsed: #{elapsed}ms"
82
+ puts
83
+ result
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ # Setup test configuration
90
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
91
+ Familia.config.encryption_keys = test_keys
92
+ Familia.config.current_key_version = :v1
93
+
94
+ # Clear cache
95
+ Thread.current[:familia_key_cache] = nil
96
+
97
+ # Define test model
98
+ class TraceTestModel < Familia::Horreum
99
+ feature :encrypted_fields
100
+ identifier_field :user_id
101
+
102
+ field :user_id
103
+ encrypted_field :password
104
+ encrypted_field :api_key, aad_fields: [:user_id]
105
+ end
106
+
107
+ puts "=== Test Scenario: Field Operations ==="
108
+ puts
109
+
110
+ puts "Creating model and setting encrypted fields..."
111
+ user = TraceTestModel.new(user_id: 'trace-user-1')
112
+
113
+ puts "Setting password (no AAD)..."
114
+ user.password = 'secret-password-123'
115
+
116
+ puts "Setting api_key (with AAD)..."
117
+ user.api_key = 'api-key-xyz-789'
118
+
119
+ puts "Reading password..."
120
+ retrieved_password = user.password
121
+ puts "Final password value: #{retrieved_password}"
122
+
123
+ puts "Reading api_key..."
124
+ retrieved_api_key = user.api_key
125
+ puts "Final api_key value: #{retrieved_api_key}"
126
+
127
+ puts
128
+ puts "=== Test Scenario: Cross-Algorithm Decryption ==="
129
+ puts
130
+
131
+ # Test cross-algorithm compatibility
132
+ aes_encrypted = Familia::Encryption.encrypt_with('aes-256-gcm', 'cross-test-data', context: 'cross-test')
133
+ puts "Decrypting AES-GCM data with default manager..."
134
+ cross_decrypted = Familia::Encryption.decrypt(aes_encrypted, context: 'cross-test')
135
+ puts "Cross-algorithm result: #{cross_decrypted}"
136
+
137
+ puts
138
+ puts "=== Method Tracing Complete ==="
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env ruby
2
+ # Debug script for testing encryption providers and diagnosing issues
3
+
4
+ require 'base64'
5
+ require_relative '../helpers/test_helpers'
6
+
7
+ puts "=== Familia Encryption Provider Diagnostics ==="
8
+ puts
9
+
10
+ # Check encryption system status
11
+ puts "1. Encryption System Status:"
12
+ puts " Status: #{Familia::Encryption.status.inspect}"
13
+ puts
14
+
15
+ # Check registry setup
16
+ puts "2. Registry Providers:"
17
+ require_relative '../../lib/familia/encryption/registry'
18
+ Familia::Encryption::Registry.setup!
19
+ Familia::Encryption::Registry.providers.each do |algo, provider_class|
20
+ puts " #{algo}: #{provider_class.name}"
21
+ puts " Available: #{provider_class.available?}"
22
+ puts " Priority: #{provider_class.priority}"
23
+ end
24
+ puts
25
+
26
+ # Setup test keys
27
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
28
+ Familia.config.encryption_keys = test_keys
29
+ Familia.config.current_key_version = :v1
30
+
31
+ # Test each provider individually
32
+ ['xchacha20poly1305', 'aes-256-gcm'].each do |algorithm|
33
+ puts "3. Testing #{algorithm.upcase} Provider:"
34
+
35
+ begin
36
+ manager = Familia::Encryption::Manager.new(algorithm: algorithm)
37
+ provider = manager.provider
38
+
39
+ puts " Provider class: #{provider.class.name}"
40
+ puts " Algorithm: #{provider.algorithm}"
41
+ puts " Nonce size: #{provider.nonce_size} bytes"
42
+ puts " Auth tag size: #{provider.auth_tag_size} bytes"
43
+
44
+ # Test encryption/decryption
45
+ test_data = "diagnostic test data for #{algorithm}"
46
+ encrypted = manager.encrypt(test_data, context: 'diagnostics')
47
+ puts " Encryption: SUCCESS (#{encrypted.length} chars)"
48
+
49
+ decrypted = manager.decrypt(encrypted, context: 'diagnostics')
50
+ success = decrypted == test_data
51
+ puts " Decryption: #{success ? 'SUCCESS' : 'FAILED'}"
52
+
53
+ if !success
54
+ puts " Expected: #{test_data.inspect}"
55
+ puts " Got: #{decrypted.inspect}"
56
+ end
57
+
58
+ rescue => e
59
+ puts " ERROR: #{e.class}: #{e.message}"
60
+ puts " Backtrace: #{e.backtrace.first(3).join(', ')}"
61
+ end
62
+
63
+ puts
64
+ end
65
+
66
+ # Test cross-algorithm compatibility
67
+ puts "4. Cross-Algorithm Compatibility Test:"
68
+ begin
69
+ xchacha_manager = Familia::Encryption::Manager.new(algorithm: 'xchacha20poly1305')
70
+ aes_manager = Familia::Encryption::Manager.new(algorithm: 'aes-256-gcm')
71
+ default_manager = Familia::Encryption::Manager.new
72
+
73
+ test_data = "cross-algorithm test"
74
+
75
+ # Encrypt with XChaCha20Poly1305
76
+ xchacha_encrypted = xchacha_manager.encrypt(test_data, context: 'cross-test')
77
+ xchacha_decrypted = default_manager.decrypt(xchacha_encrypted, context: 'cross-test')
78
+ puts " XChaCha20Poly1305 -> Default: #{xchacha_decrypted == test_data ? 'SUCCESS' : 'FAILED'}"
79
+
80
+ # Encrypt with AES-GCM
81
+ aes_encrypted = aes_manager.encrypt(test_data, context: 'cross-test')
82
+ aes_decrypted = default_manager.decrypt(aes_encrypted, context: 'cross-test')
83
+ puts " AES-GCM -> Default: #{aes_decrypted == test_data ? 'SUCCESS' : 'FAILED'}"
84
+
85
+ rescue => e
86
+ puts " ERROR: #{e.class}: #{e.message}"
87
+ end
88
+ puts
89
+
90
+ # Test high-level API
91
+ puts "5. High-Level API Test:"
92
+ begin
93
+ encrypted_high = Familia::Encryption.encrypt_with('aes-256-gcm', 'high-level test', context: 'api-test')
94
+ puts " encrypt_with: SUCCESS"
95
+
96
+ # Parse encrypted data to verify structure
97
+ require 'json'
98
+ parsed = JSON.parse(encrypted_high, symbolize_names: true)
99
+ puts " Algorithm stored: #{parsed[:algorithm]}"
100
+ puts " Key version: #{parsed[:key_version]}"
101
+
102
+ decrypted_high = Familia::Encryption.decrypt(encrypted_high, context: 'api-test')
103
+ puts " decrypt: #{decrypted_high == 'high-level test' ? 'SUCCESS' : 'FAILED'}"
104
+
105
+ rescue => e
106
+ puts " ERROR: #{e.class}: #{e.message}"
107
+ end
108
+
109
+ puts
110
+ puts "=== Diagnostics Complete ==="
@@ -4,7 +4,6 @@
4
4
  # bug in Tryouts 3.1 that prevents the setup instance vars from
5
5
  # being available to the testcases.
6
6
 
7
- require_relative '../../lib/familia'
8
7
  require_relative '../helpers/test_helpers'
9
8
 
10
9
  Familia.debug = false
@@ -1,6 +1,5 @@
1
1
  # try/edge_cases/json_serialization_try.rb
2
2
 
3
- require_relative '../../lib/familia'
4
3
  require_relative '../helpers/test_helpers'
5
4
 
6
5
  Familia.debug = false
@@ -17,7 +17,7 @@ result
17
17
  #=!> StandardError
18
18
 
19
19
  ## prefixed field names work as expected
20
- TestClass2 = Class.new(Familia::Horreum) do
20
+ ExampleTestClass = Class.new(Familia::Horreum) do
21
21
  identifier_field :email
22
22
  field :email
23
23
  field :secret_ttl
@@ -25,7 +25,7 @@ TestClass2 = Class.new(Familia::Horreum) do
25
25
  field :dbclient_config
26
26
  end
27
27
 
28
- user = TestClass2.new(email: 'test@example.com')
28
+ user = ExampleTestClass.new(email: 'test@example.com')
29
29
  user.secret_ttl = 3600
30
30
  user.user_db = 5
31
31
  user.dbclient_config = { host: 'localhost' }
@@ -39,7 +39,7 @@ user.delete!
39
39
  result
40
40
  #=> true
41
41
 
42
- ## reserved methods still work normally
42
+ ## Reserved methods still work normally
43
43
  TestClass3 = Class.new(Familia::Horreum) do
44
44
  # Note: Does not enable expiration feature
45
45
  identifier_field :email
@@ -55,20 +55,51 @@ user
55
55
  #==> _.respond_to?(:logical_database)
56
56
  #==> _.respond_to?(:dbclient)
57
57
 
58
+ ## Attempting to pass default_expiration as a field value when instantiating,
59
+ ## when expiration feature is enabled. It doesn't actually change the default
60
+ ## expiration for the instance b/c "default_expiration" is not a regular field.
61
+ TestClassWithExpirationEnabled1 = Class.new(Familia::Horreum) do
62
+ feature :expiration
63
+ identifier_field :email
64
+ field :email
65
+ end
66
+
67
+ user = TestClassWithExpirationEnabled1.new(email: 'test@example.com', default_expiration: 3600)
68
+ user.default_expiration
69
+ #=> 0
58
70
 
59
- ## Attempting to set default_expiration on an instance with expiration feature enabled
60
- TestClass4 = Class.new(Familia::Horreum) do
71
+ ## Attempting to set default_expiration for an instance when
72
+ ## the feature is enabled should work
73
+ TestClassWithExpirationEnabled2 = Class.new(Familia::Horreum) do
61
74
  feature :expiration
62
75
  identifier_field :email
63
76
  field :email
64
- field :default_expiration
65
77
  end
66
78
 
67
- user = TestClass4.new(email: 'test@example.com', default_expiration: 3600)
68
- user.save
69
- user.delete!
70
- user
71
- #==> _.default_expiration == 3600
79
+ user = TestClassWithExpirationEnabled2.new(email: 'test@example.com')
80
+ user.default_expiration = 3601
81
+ user.default_expiration
82
+ #=> 3601
83
+
84
+ ## Attempting to pass default_expiration as a field value when instantiating,
85
+ ## when expiration feature is disabled and then trying to access that value
86
+ ## simply raises a NoMethodError error.
87
+ TestClassWithExpirationDisabled = Class.new(Familia::Horreum) do
88
+ identifier_field :email
89
+ field :email
90
+ end
91
+
92
+ user = TestClassWithExpirationDisabled.new(email: 'test@example.com', default_expiration: 3600)
93
+ user.default_expiration
94
+ #=!> NoMethodError
95
+
96
+ ## Attempting to add a field with a reserved name should raise an error
97
+ TestClassWithExpirationDisabled = Class.new(Familia::Horreum) do
98
+ identifier_field :email
99
+ field :email
100
+ field :default_expiration
101
+ end
102
+ ##=!> NoMethodError
72
103
 
73
104
  ## prefixed field names work as expected
74
105
  TestClass5 = Class.new(Familia::Horreum) do
@@ -0,0 +1,192 @@
1
+ # try/encryption/debug2_try.rb
2
+
3
+ # - Tests configuration persistence between test sections
4
+ # - Validates that config can be set and accessed in tryouts
5
+
6
+ require 'base64'
7
+
8
+ require_relative '../helpers/test_helpers'
9
+ require 'familia/encryption/providers/xchacha20_poly1305_provider'
10
+
11
+ # SETUP
12
+ Familia.config.encryption_keys = {
13
+ v1: Base64.strict_encode64('a' * 32)
14
+ }
15
+ Familia.config.current_key_version = :v1
16
+
17
+ ## Check config in test
18
+ keys = Familia.config.encryption_keys
19
+ version = Familia.config.current_key_version
20
+ [keys.nil?, version.nil?]
21
+ #=> [false, false]
22
+
23
+ ## Try basic encryption in test
24
+ Familia.config.encryption_keys = {v1: Base64.strict_encode64('a' * 32)}
25
+ Familia.config.current_key_version = :v1
26
+ result = Familia::Encryption.encrypt('test', context: 'test')
27
+ result.nil?
28
+ #=> false
29
+
30
+ ## XChaCha20Poly1305Provider is available when RbNaCl is loaded
31
+ @provider_class = Familia::Encryption::Providers::XChaCha20Poly1305Provider
32
+ @provider_class.available?
33
+ #=> true
34
+
35
+ ## Provider has highest priority
36
+ @provider_class.priority
37
+ #=> 100
38
+
39
+ ## derive_key generates 32-byte key from master key and context
40
+ provider = @provider_class.new
41
+ master_key = 'a' * 32
42
+ context = 'TestModel:field:user123'
43
+ derived_key = provider.derive_key(master_key, context)
44
+ derived_key.bytesize
45
+ #=> 32
46
+
47
+ ## derive_key with same inputs produces same output
48
+ provider = @provider_class.new
49
+ master_key = 'a' * 32
50
+ context = 'TestModel:field:user123'
51
+ key1 = provider.derive_key(master_key, context)
52
+ key2 = provider.derive_key(master_key, context)
53
+ key1 == key2
54
+ #=> true
55
+
56
+ ## derive_key with different contexts produces different keys
57
+ provider = @provider_class.new
58
+ master_key = 'a' * 32
59
+ context1 = 'TestModel:field:user123'
60
+ context2 = 'TestModel:field:user456'
61
+ key1 = provider.derive_key(master_key, context1)
62
+ key2 =
63
+ provider.derive_key(master_key, context2)
64
+ key1 != key2
65
+ #=> true
66
+
67
+ ## derive_key with custom personalization works
68
+ provider = @provider_class.new
69
+ master_key = 'a' * 32
70
+ context = 'TestModel:field:user123'
71
+ personal = 'custom_app_v2'
72
+ derived_key = provider.derive_key(master_key, context, personal: personal)
73
+ derived_key.bytesize
74
+ #=> 32
75
+
76
+ ## derive_key rejects personalization string with null bytes
77
+ provider = @provider_class.new
78
+ master_key = 'a' * 32
79
+ context = 'TestModel:field:user123'
80
+ personal_with_null = "app\0version"
81
+ begin
82
+ provider.derive_key(master_key, context, personal: personal_with_null)
83
+ "should_not_reach_here"
84
+ rescue Familia::EncryptionError => e
85
+ e.message
86
+ end
87
+ #=> "Personalization string must not contain null bytes"
88
+
89
+ ## derive_key rejects config personalization with null bytes
90
+ provider = @provider_class.new
91
+ master_key = 'a' * 32
92
+ context = 'TestModel:field:user123'
93
+ # Set config with null byte
94
+ original_personal = Familia.config.encryption_personalization
95
+ Familia.config.encryption_personalization = "bad\0config"
96
+ begin
97
+ provider.derive_key(master_key, context)
98
+ "should_not_reach_here"
99
+ rescue Familia::EncryptionError => e
100
+ e.message
101
+ ensure
102
+ Familia.config.encryption_personalization = original_personal
103
+ end
104
+ #=> "Personalization string must not contain null bytes"
105
+
106
+ ## derive_key validates master key length
107
+ provider = @provider_class.new
108
+ short_key = 'a' * 16 # Too short
109
+ context = 'TestModel:field:user123'
110
+ begin
111
+ provider.derive_key(short_key, context)
112
+ "should_not_reach_here"
113
+ rescue Familia::EncryptionError => e
114
+ e.message
115
+ end
116
+ #=> "Key must be at least 32 bytes"
117
+
118
+ ## derive_key rejects nil master key
119
+ provider = @provider_class.new
120
+ context = 'TestModel:field:user123'
121
+ begin
122
+ provider.derive_key(nil, context)
123
+ "should_not_reach_here"
124
+ rescue Familia::EncryptionError => e
125
+ e.message
126
+ end
127
+ #=> "Key cannot be nil"
128
+
129
+ ## encrypt/decrypt round trip with derived key works
130
+ provider = @provider_class.new
131
+ master_key = 'a' * 32
132
+ context = 'TestModel:field:user123'
133
+ derived_key = provider.derive_key(master_key, context)
134
+ plaintext = 'sensitive data'
135
+ encrypted_data = provider.encrypt(plaintext, derived_key)
136
+ decrypted = provider.decrypt(
137
+ encrypted_data[:ciphertext],
138
+ derived_key,
139
+ encrypted_data[:nonce],
140
+ encrypted_data[:auth_tag]
141
+ )
142
+ decrypted
143
+ #=> "sensitive data"
144
+
145
+ ## encrypt with additional data and derived key
146
+ provider = @provider_class.new
147
+ master_key = 'a' * 32
148
+ context = 'TestModel:field:user123'
149
+ derived_key = provider.derive_key(master_key, context)
150
+ plaintext = 'sensitive data'
151
+ additional_data = 'user_id:123'
152
+ encrypted_data = provider.encrypt(plaintext, derived_key, additional_data)
153
+ decrypted = provider.decrypt(
154
+ encrypted_data[:ciphertext],
155
+ derived_key,
156
+ encrypted_data[:nonce],
157
+ encrypted_data[:auth_tag],
158
+ additional_data
159
+ )
160
+ decrypted
161
+ #=> "sensitive data"
162
+
163
+ ## generate_nonce produces correct size
164
+ provider = @provider_class.new
165
+ nonce = provider.generate_nonce
166
+ nonce.bytesize
167
+ #=> 24
168
+
169
+ ## generate_nonce produces unique values
170
+ provider = @provider_class.new
171
+ nonce1 = provider.generate_nonce
172
+ nonce2 = provider.generate_nonce
173
+ nonce1 != nonce2
174
+ #=> true
175
+
176
+ ## secure_wipe works with valid key
177
+ provider = @provider_class.new
178
+ key = 'a' * 32
179
+ provider.secure_wipe(key)
180
+ # Should not raise error
181
+ true
182
+ #=> true
183
+
184
+ ## secure_wipe handles nil key gracefully
185
+ provider = @provider_class.new
186
+ provider.secure_wipe(nil)
187
+ # Should not raise error
188
+ true
189
+ #=> true
190
+
191
+ # TEARDOWN
192
+ Thread.current[:familia_key_cache]&.clear if Thread.current[:familia_key_cache]