familia 2.0.0.pre5 → 2.0.0.pre7

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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -10
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +4 -3
  9. data/docs/wiki/API-Reference.md +95 -18
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +631 -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 +82 -15
  17. data/docs/wiki/Implementation-Guide.md +126 -33
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/Relationships-Guide.md +684 -0
  20. data/docs/wiki/Security-Model.md +65 -25
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/examples/bit_encoding_integration.rb +237 -0
  23. data/examples/redis_command_validation_example.rb +231 -0
  24. data/examples/relationships_basic.rb +273 -0
  25. data/lib/familia/base.rb +1 -1
  26. data/lib/familia/connection.rb +3 -3
  27. data/lib/familia/data_type/types/counter.rb +38 -0
  28. data/lib/familia/data_type/types/hashkey.rb +18 -0
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/data_type/types/string.rb +9 -2
  31. data/lib/familia/data_type.rb +9 -6
  32. data/lib/familia/encryption/encrypted_data.rb +137 -0
  33. data/lib/familia/encryption/manager.rb +21 -4
  34. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  35. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  36. data/lib/familia/encryption.rb +1 -1
  37. data/lib/familia/errors.rb +17 -3
  38. data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
  39. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  40. data/lib/familia/features/encrypted_fields.rb +413 -4
  41. data/lib/familia/features/expiration.rb +319 -33
  42. data/lib/familia/features/quantization.rb +385 -44
  43. data/lib/familia/features/relationships/cascading.rb +438 -0
  44. data/lib/familia/features/relationships/indexing.rb +370 -0
  45. data/lib/familia/features/relationships/membership.rb +503 -0
  46. data/lib/familia/features/relationships/permission_management.rb +264 -0
  47. data/lib/familia/features/relationships/querying.rb +620 -0
  48. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  49. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  50. data/lib/familia/features/relationships/tracking.rb +379 -0
  51. data/lib/familia/features/relationships.rb +466 -0
  52. data/lib/familia/features/safe_dump.rb +1 -1
  53. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  54. data/lib/familia/features/transient_fields.rb +192 -10
  55. data/lib/familia/features.rb +2 -1
  56. data/lib/familia/field_type.rb +5 -2
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  58. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  63. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
  64. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +17 -17
  67. data/lib/familia/validation/command_recorder.rb +336 -0
  68. data/lib/familia/validation/expectations.rb +519 -0
  69. data/lib/familia/validation/test_helpers.rb +443 -0
  70. data/lib/familia/validation/validator.rb +412 -0
  71. data/lib/familia/validation.rb +140 -0
  72. data/lib/familia/version.rb +1 -1
  73. data/lib/familia.rb +1 -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 -4
  77. data/try/core/familia_try.rb +1 -1
  78. data/try/core/persistence_operations_try.rb +297 -0
  79. data/try/data_types/counter_try.rb +93 -0
  80. data/try/data_types/lock_try.rb +133 -0
  81. data/try/debugging/debug_aad_process.rb +82 -0
  82. data/try/debugging/debug_concealed_internal.rb +59 -0
  83. data/try/debugging/debug_concealed_reveal.rb +61 -0
  84. data/try/debugging/debug_context_aad.rb +68 -0
  85. data/try/debugging/debug_context_simple.rb +80 -0
  86. data/try/debugging/debug_cross_context.rb +62 -0
  87. data/try/debugging/debug_database_load.rb +64 -0
  88. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  89. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  90. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  91. data/try/debugging/debug_field_decrypt.rb +74 -0
  92. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  93. data/try/debugging/debug_load_path.rb +66 -0
  94. data/try/debugging/debug_method_definition.rb +46 -0
  95. data/try/debugging/debug_method_resolution.rb +41 -0
  96. data/try/debugging/debug_minimal.rb +24 -0
  97. data/try/debugging/debug_provider.rb +68 -0
  98. data/try/debugging/debug_secure_behavior.rb +73 -0
  99. data/try/debugging/debug_string_class.rb +46 -0
  100. data/try/debugging/debug_test.rb +46 -0
  101. data/try/debugging/debug_test_design.rb +80 -0
  102. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  103. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  104. data/try/edge_cases/string_coercion_try.rb +2 -0
  105. data/try/encryption/encryption_core_try.rb +6 -4
  106. data/try/features/categorical_permissions_try.rb +515 -0
  107. data/try/features/encrypted_fields_core_try.rb +19 -11
  108. data/try/features/encrypted_fields_integration_try.rb +66 -70
  109. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  110. data/try/features/encrypted_fields_security_try.rb +151 -144
  111. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  112. data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
  113. data/try/features/encryption_fields/context_isolation_try.rb +30 -8
  114. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  115. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  116. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  117. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  118. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  119. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  120. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  121. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  122. data/try/features/feature_dependencies_try.rb +3 -3
  123. data/try/features/relationships_edge_cases_try.rb +145 -0
  124. data/try/features/relationships_performance_minimal_try.rb +132 -0
  125. data/try/features/relationships_performance_simple_try.rb +155 -0
  126. data/try/features/relationships_performance_try.rb +420 -0
  127. data/try/features/relationships_performance_working_try.rb +144 -0
  128. data/try/features/relationships_try.rb +237 -0
  129. data/try/features/safe_dump_try.rb +3 -0
  130. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  131. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  132. data/try/features/transient_fields_core_try.rb +1 -1
  133. data/try/features/transient_fields_integration_try.rb +1 -1
  134. data/try/helpers/test_helpers.rb +26 -1
  135. data/try/horreum/base_try.rb +14 -8
  136. data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
  137. data/try/horreum/initialization_try.rb +1 -1
  138. data/try/horreum/relations_try.rb +2 -2
  139. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  140. data/try/horreum/serialization_try.rb +39 -4
  141. data/try/models/customer_safe_dump_try.rb +1 -1
  142. data/try/models/customer_try.rb +1 -1
  143. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  144. data/try/validation/command_validation_try.rb.disabled +207 -0
  145. data/try/validation/performance_validation_try.rb.disabled +324 -0
  146. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  147. metadata +81 -12
  148. data/TEST_COVERAGE.md +0 -40
  149. data/lib/familia/features/relatable_objects.rb +0 -125
  150. data/lib/familia/horreum/serialization.rb +0 -473
  151. data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
4
+ ENV['TEST'] = 'true' # Mark as test environment
5
+ require 'familia'
6
+ require_relative 'try/helpers/test_helpers'
7
+
8
+ puts "Testing ConcealedString.reveal error handling..."
9
+
10
+ class TestModel < Familia::Horreum
11
+ feature :encrypted_fields
12
+ identifier_field :id
13
+ field :id
14
+ encrypted_field :secret
15
+ end
16
+
17
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
18
+ Familia.config.encryption_keys = test_keys
19
+ Familia.config.current_key_version = :v1
20
+
21
+ # Clean database
22
+ Familia.dbclient.flushdb
23
+
24
+ puts "\n=== CONCEALED STRING REVEAL ERROR PATH ==="
25
+
26
+ # Create and save model (encrypt with AAD = nil)
27
+ model = TestModel.new(id: 'test1')
28
+ model.secret = 'plaintext-secret'
29
+ model.save
30
+
31
+ # Load model from database (decrypt with AAD = "test1")
32
+ loaded_model = TestModel.load('test1')
33
+
34
+ puts "Loaded model secret class: #{loaded_model.secret.class}"
35
+
36
+ # Test ConcealedString.reveal directly
37
+ concealed_string = loaded_model.secret
38
+ puts "ConcealedString object: #{concealed_string.inspect}"
39
+
40
+ puts "\nTesting ConcealedString.reveal..."
41
+ begin
42
+ result = concealed_string.reveal do |plaintext|
43
+ puts "Inside reveal block, plaintext: #{plaintext}"
44
+ plaintext # Return the plaintext
45
+ end
46
+ puts "Reveal result: #{result}"
47
+ puts "Result class: #{result.class}"
48
+ rescue => e
49
+ puts "Reveal ERROR: #{e.class}: #{e.message}"
50
+ puts e.backtrace.first(5)
51
+ end
52
+
53
+ puts "\nTesting ConcealedString.reveal_for_testing..."
54
+ begin
55
+ result = concealed_string.reveal_for_testing
56
+ puts "reveal_for_testing result: #{result}"
57
+ puts "Result class: #{result.class}"
58
+ rescue => e
59
+ puts "reveal_for_testing ERROR: #{e.class}: #{e.message}"
60
+ puts e.backtrace.first(5)
61
+ end
@@ -0,0 +1,68 @@
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 "Debugging context and AAD generation..."
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 ModelA < Familia::Horreum
16
+ feature :encrypted_fields
17
+ identifier_field :id
18
+ field :id
19
+ encrypted_field :api_key
20
+ end
21
+
22
+ class ModelB < Familia::Horreum
23
+ feature :encrypted_fields
24
+ identifier_field :id
25
+ field :id
26
+ encrypted_field :api_key
27
+ end
28
+
29
+ model_a = ModelA.new(id: 'same-id')
30
+ model_b = ModelB.new(id: 'same-id')
31
+
32
+ # Get the field type for each model
33
+ field_type_a = ModelA.fields[:api_key]
34
+ field_type_b = ModelB.fields[:api_key]
35
+
36
+ puts "=== Context and AAD Analysis ==="
37
+
38
+ # Test context generation
39
+ context_a = field_type_a.send(:build_context, model_a)
40
+ context_b = field_type_b.send(:build_context, model_b)
41
+
42
+ puts "ModelA context: #{context_a}"
43
+ puts "ModelB context: #{context_b}"
44
+ puts "Contexts match: #{context_a == context_b}"
45
+
46
+ # Test AAD generation
47
+ aad_a = field_type_a.send(:build_aad, model_a)
48
+ aad_b = field_type_b.send(:build_aad, model_b)
49
+
50
+ puts "\nModelA AAD: #{aad_a}"
51
+ puts "ModelB AAD: #{aad_b}"
52
+ puts "AADs match: #{aad_a == aad_b}"
53
+
54
+ puts "\n=== Testing different identifiers ==="
55
+ model_a2 = ModelA.new(id: 'different-id')
56
+ model_b2 = ModelB.new(id: 'different-id')
57
+
58
+ context_a2 = field_type_a.send(:build_context, model_a2)
59
+ context_b2 = field_type_b.send(:build_context, model_b2)
60
+ aad_a2 = field_type_a.send(:build_aad, model_a2)
61
+ aad_b2 = field_type_b.send(:build_aad, model_b2)
62
+
63
+ puts "ModelA2 context: #{context_a2}"
64
+ puts "ModelB2 context: #{context_b2}"
65
+ puts "A2-B2 contexts match: #{context_a2 == context_b2}"
66
+ puts "ModelA2 AAD: #{aad_a2}"
67
+ puts "ModelB2 AAD: #{aad_b2}"
68
+ puts "A2-B2 AADs match: #{aad_a2 == aad_b2}"
@@ -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
+
7
+ puts "Understanding the issue with cross-context decryption..."
8
+
9
+ # Setup encryption keys
10
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
11
+ Familia.config.encryption_keys = test_keys
12
+ Familia.config.current_key_version = :v1
13
+
14
+ class ModelA < Familia::Horreum
15
+ feature :encrypted_fields
16
+ identifier_field :id
17
+ field :id
18
+ encrypted_field :api_key
19
+ end
20
+
21
+ class ModelB < Familia::Horreum
22
+ feature :encrypted_fields
23
+ identifier_field :id
24
+ field :id
25
+ encrypted_field :api_key
26
+ end
27
+
28
+ model_a = ModelA.new(id: 'same-id')
29
+ model_b = ModelB.new(id: 'same-id')
30
+
31
+ puts "ModelA class: #{model_a.class}"
32
+ puts "ModelB class: #{model_b.class}"
33
+ puts "Same class?: #{model_a.class == model_b.class}"
34
+
35
+ # Inspect what context would be built
36
+ puts "\nContext analysis:"
37
+ puts "ModelA identifier: #{model_a.identifier}"
38
+ puts "ModelB identifier: #{model_b.identifier}"
39
+
40
+ # Let's see what happens with the field types
41
+ modelA_api_key_type = nil
42
+ modelB_api_key_type = nil
43
+
44
+ ModelA.field_types.each do |name, type|
45
+ if name == :api_key
46
+ modelA_api_key_type = type
47
+ break
48
+ end
49
+ end
50
+
51
+ ModelB.field_types.each do |name, type|
52
+ if name == :api_key
53
+ modelB_api_key_type = type
54
+ break
55
+ end
56
+ end
57
+
58
+ puts "ModelA api_key type: #{modelA_api_key_type}"
59
+ puts "ModelB api_key type: #{modelB_api_key_type}"
60
+ puts "Same type object?: #{modelA_api_key_type.object_id == modelB_api_key_type.object_id}"
61
+
62
+ # Check what context and AAD would be generated
63
+ if modelA_api_key_type && modelB_api_key_type
64
+ context_a = "#{model_a.class.name}:api_key:#{model_a.identifier}"
65
+ context_b = "#{model_b.class.name}:api_key:#{model_b.identifier}"
66
+
67
+ puts "\nExpected contexts:"
68
+ puts "ModelA context: #{context_a}"
69
+ puts "ModelB context: #{context_b}"
70
+ puts "Contexts should be different: #{context_a != context_b}"
71
+
72
+ # AAD should be identifier when no aad_fields
73
+ aad_a = model_a.identifier
74
+ aad_b = model_b.identifier
75
+
76
+ puts "\nExpected AADs:"
77
+ puts "ModelA AAD: #{aad_a}"
78
+ puts "ModelB AAD: #{aad_b}"
79
+ puts "AADs are same: #{aad_a == aad_b}"
80
+ end
@@ -0,0 +1,62 @@
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 "Debugging cross-context validation..."
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 ModelA < Familia::Horreum
16
+ feature :encrypted_fields
17
+ identifier_field :id
18
+ field :id
19
+ encrypted_field :api_key
20
+ end
21
+
22
+ class ModelB < 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 = ModelA.new(id: 'same-id')
33
+ model_b = ModelB.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: #{cipher_a.class} - #{cipher_a.encrypted_value}"
42
+ puts "cipher_b: #{cipher_b.class} - #{cipher_b.encrypted_value}"
43
+
44
+ # Now try cross-context access
45
+ puts "\n=== Cross-context test ==="
46
+ model_a.instance_variable_set(:@api_key, cipher_b)
47
+ puts "Set cipher_b into model_a"
48
+
49
+ begin
50
+ result = model_a.api_key
51
+ puts "Got result: #{result.class} - should be ConcealedString"
52
+
53
+ # Try to reveal it
54
+ result.reveal do |plaintext|
55
+ puts "Successfully revealed: #{plaintext}"
56
+ end
57
+ puts "ERROR: Cross-context decryption should have failed!"
58
+ rescue Familia::EncryptionError => e
59
+ puts "SUCCESS: Got expected encryption error: #{e.message}"
60
+ rescue => e
61
+ puts "Got unexpected error: #{e.class}: #{e.message}"
62
+ end
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
4
+ ENV['TEST'] = 'true' # Mark as test environment
5
+ require 'familia'
6
+ require_relative 'try/helpers/test_helpers'
7
+
8
+ puts "Testing database load vs in-memory encryption..."
9
+
10
+ class TestModel < Familia::Horreum
11
+ feature :encrypted_fields
12
+ identifier_field :id
13
+ field :id
14
+ field :title
15
+ encrypted_field :secret
16
+ end
17
+
18
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
19
+ Familia.config.encryption_keys = test_keys
20
+ Familia.config.current_key_version = :v1
21
+
22
+ # Clean database
23
+ Familia.dbclient.flushdb
24
+
25
+ puts "\n=== PHASE 1: In-memory object ==="
26
+ model = TestModel.new(id: 'test1')
27
+ model.title = 'Test Title'
28
+ puts "PRE-ENCRYPT: model.exists? = #{model.exists?}"
29
+ model.secret = 'plaintext-secret'
30
+
31
+ puts "In-memory secret class: #{model.secret.class}"
32
+ puts "In-memory secret value: #{model.secret}"
33
+
34
+ model.secret.reveal do |plaintext|
35
+ puts "In-memory reveal: #{plaintext}"
36
+ end
37
+
38
+ puts "\n=== PHASE 2: Save to database ==="
39
+ model.save
40
+
41
+ puts "Keys in database: #{Familia.dbclient.keys('*')}"
42
+ puts "Hash contents: #{Familia.dbclient.hgetall('testmodel:test1:object')}"
43
+
44
+ raw_secret = Familia.dbclient.hget('testmodel:test1:object', 'secret')
45
+ puts "Raw secret from DB: #{raw_secret}"
46
+
47
+ puts "\n=== PHASE 3: Load from database ==="
48
+ loaded_model = TestModel.load('test1')
49
+
50
+ if loaded_model
51
+ puts "Loaded model exists: #{loaded_model.exists?}"
52
+ puts "Loaded secret class: #{loaded_model.secret.class}"
53
+ puts "Loaded secret value: #{loaded_model.secret}"
54
+
55
+ begin
56
+ loaded_model.secret.reveal do |plaintext|
57
+ puts "Loaded reveal: #{plaintext}"
58
+ end
59
+ rescue => e
60
+ puts "Loaded reveal ERROR: #{e.class}: #{e.message}"
61
+ end
62
+ else
63
+ puts "Failed to load model"
64
+ end
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
4
+ ENV['TEST'] = 'true' # Mark as test environment
5
+ require 'familia'
6
+ require_relative 'try/helpers/test_helpers'
7
+
8
+ puts "Testing encrypted_json? method..."
9
+
10
+ class TestModel < Familia::Horreum
11
+ feature :encrypted_fields
12
+ identifier_field :id
13
+ field :id
14
+ encrypted_field :secret
15
+ end
16
+
17
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
18
+ Familia.config.encryption_keys = test_keys
19
+ Familia.config.current_key_version = :v1
20
+
21
+ field_type = TestModel.field_types[:secret]
22
+
23
+ # Test with actual encrypted JSON from the load path
24
+ encrypted_json = '{"algorithm":"xchacha20poly1305","nonce":"RDK0GSY3Vbrbv7OAgol10bHOmderAExt","ciphertext":"uo8j6Pm6tV68BcvqK5maXQ==","auth_tag":"5Cr1QgTnajnWIji0fsQP0g==","key_version":"v1"}'
25
+
26
+ puts "Testing encrypted JSON: #{encrypted_json[0..80]}..."
27
+ puts "String class: #{encrypted_json.class}"
28
+ puts "Is string?: #{encrypted_json.is_a?(String)}"
29
+
30
+ puts "\nManual JSON parsing:"
31
+ begin
32
+ parsed = JSON.parse(encrypted_json)
33
+ puts "Parsed successfully: #{parsed.class}"
34
+ puts "Is hash?: #{parsed.is_a?(Hash)}"
35
+ puts "Has algorithm key?: #{parsed.key?('algorithm')}"
36
+ puts "Algorithm value: #{parsed['algorithm']}"
37
+ rescue => e
38
+ puts "JSON parse error: #{e.class}: #{e.message}"
39
+ end
40
+
41
+ puts "\nTesting field_type.encrypted_json? method:"
42
+ result = field_type.encrypted_json?(encrypted_json)
43
+ puts "encrypted_json? result: #{result}"
44
+
45
+ puts "\nTesting with symbol keys:"
46
+ begin
47
+ parsed_sym = JSON.parse(encrypted_json, symbolize_names: true)
48
+ puts "Parsed with symbols: #{parsed_sym.class}"
49
+ puts "Has :algorithm key?: #{parsed_sym.key?(:algorithm)}"
50
+ puts "Has 'algorithm' key?: #{parsed_sym.key?('algorithm')}"
51
+ rescue => e
52
+ puts "Symbol parse error: #{e.class}: #{e.message}"
53
+ end
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
4
+ ENV['TEST'] = 'true' # Mark as test environment
5
+ require 'familia'
6
+ require_relative 'try/helpers/test_helpers'
7
+
8
+ puts "Step-by-step debugging of encrypted_json? method..."
9
+
10
+ class TestModel < Familia::Horreum
11
+ feature :encrypted_fields
12
+ identifier_field :id
13
+ field :id
14
+ encrypted_field :secret
15
+ end
16
+
17
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
18
+ Familia.config.encryption_keys = test_keys
19
+ Familia.config.current_key_version = :v1
20
+
21
+ field_type = TestModel.field_types[:secret]
22
+
23
+ # Monkey patch the encrypted_json? method to add debugging
24
+ class Familia::EncryptedFieldType
25
+ def encrypted_json?(data)
26
+ puts "\n=== encrypted_json? DEBUG ==="
27
+ puts "Input data: #{data}"
28
+ puts "Data class: #{data.class}"
29
+ puts "Is String?: #{data.is_a?(String)}"
30
+
31
+ return false unless data.is_a?(String)
32
+ puts "Passed String check"
33
+
34
+ begin
35
+ puts "Attempting JSON.parse..."
36
+ parsed = JSON.parse(data)
37
+ puts "Parsed result: #{parsed}"
38
+ puts "Parsed class: #{parsed.class}"
39
+ puts "Is Hash?: #{parsed.is_a?(Hash)}"
40
+
41
+ if parsed.is_a?(Hash)
42
+ puts "Hash keys: #{parsed.keys}"
43
+ puts "Has 'algorithm' key?: #{parsed.key?('algorithm')}"
44
+ result = parsed.key?('algorithm')
45
+ puts "Final result: #{result}"
46
+ return result
47
+ else
48
+ puts "Not a hash, returning false"
49
+ return false
50
+ end
51
+ rescue JSON::ParserError => e
52
+ puts "JSON parse error: #{e.message}"
53
+ false
54
+ end
55
+ end
56
+ end
57
+
58
+ encrypted_json = '{"algorithm":"xchacha20poly1305","nonce":"RDK0GSY3Vbrbv7OAgol10bHOmderAExt","ciphertext":"uo8j6Pm6tV68BcvqK5maXQ==","auth_tag":"5Cr1QgTnajnWIji0fsQP0g==","key_version":"v1"}'
59
+
60
+ puts "Calling field_type.encrypted_json?..."
61
+ result = field_type.encrypted_json?(encrypted_json)
62
+ puts "\nFINAL RESULT: #{result}"
@@ -0,0 +1,54 @@
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 "Investigating exists? lifecycle..."
9
+
10
+ class TestModel < Familia::Horreum
11
+ feature :encrypted_fields
12
+ identifier_field :id
13
+ field :id
14
+ encrypted_field :secret
15
+ end
16
+
17
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
18
+ Familia.config.encryption_keys = test_keys
19
+ Familia.config.current_key_version = :v1
20
+
21
+ # Clean database
22
+ Familia.dbclient.flushdb
23
+
24
+ # Override the exists? method to add logging
25
+ class TestModel
26
+ alias_method :original_exists?, :exists?
27
+ def exists?
28
+ result = original_exists?
29
+ puts "EXISTS? called - result: #{result} (identifier: #{identifier})" if ENV['TEST']
30
+ result
31
+ end
32
+ end
33
+
34
+ puts "\n=== CREATION AND SAVE ==="
35
+ model = TestModel.new(id: 'test1')
36
+ puts "After new - exists?: #{model.exists?}"
37
+ model.secret = 'plaintext-secret'
38
+ puts "After setting secret - exists?: #{model.exists?}"
39
+ model.save
40
+ puts "After save - exists?: #{model.exists?}"
41
+
42
+ puts "\n=== LOAD FROM DATABASE ==="
43
+ puts "About to load..."
44
+ loaded_model = TestModel.load('test1')
45
+ puts "After load - exists?: #{loaded_model.exists?}"
46
+
47
+ puts "\n=== TRYING TO ACCESS SECRET ==="
48
+ begin
49
+ loaded_model.secret.reveal do |plaintext|
50
+ puts "Successfully revealed: #{plaintext}"
51
+ end
52
+ rescue => e
53
+ puts "Failed to reveal: #{e.class}: #{e.message}"
54
+ end
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
4
+ ENV['TEST'] = 'true' # Mark as test environment
5
+ require 'familia'
6
+ require_relative 'try/helpers/test_helpers'
7
+
8
+ puts "Testing field-level decryption path..."
9
+
10
+ class TestModel < Familia::Horreum
11
+ feature :encrypted_fields
12
+ identifier_field :id
13
+ field :id
14
+ encrypted_field :secret
15
+ end
16
+
17
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
18
+ Familia.config.encryption_keys = test_keys
19
+ Familia.config.current_key_version = :v1
20
+
21
+ # Clean database
22
+ Familia.dbclient.flushdb
23
+
24
+ puts "\n=== FIELD-LEVEL DECRYPTION PATH ==="
25
+
26
+ # Create and save model
27
+ model = TestModel.new(id: 'test1')
28
+ model.secret = 'plaintext-secret'
29
+ model.save
30
+
31
+ # Load model from database
32
+ loaded_model = TestModel.load('test1')
33
+
34
+ # Get the field type and test direct decryption
35
+ puts "Available field types: #{TestModel.field_types.inspect}"
36
+ secret_field_type = TestModel.field_types[:secret]
37
+ puts "Field type class: #{secret_field_type.class}"
38
+
39
+ # Get the raw encrypted data
40
+ raw_encrypted = Familia.dbclient.hget('testmodel:test1:object', 'secret')
41
+ puts "Raw encrypted from DB: #{raw_encrypted}"
42
+
43
+ puts "\n=== DIRECT FIELD TYPE DECRYPTION ==="
44
+
45
+ # Test the field type decrypt_value method directly
46
+ puts "Testing field_type.decrypt_value with loaded model..."
47
+ begin
48
+ direct_decrypted = secret_field_type.decrypt_value(loaded_model, raw_encrypted)
49
+ puts "Direct field decrypt result: #{direct_decrypted}"
50
+ puts "Result class: #{direct_decrypted.class}"
51
+ rescue => e
52
+ puts "Direct field decrypt ERROR: #{e.class}: #{e.message}"
53
+ puts e.backtrace.first(5)
54
+ end
55
+
56
+ puts "\n=== AAD INVESTIGATION ==="
57
+ puts "loaded_model.exists?: #{loaded_model.exists?}"
58
+ puts "loaded_model.identifier: #{loaded_model.identifier}"
59
+
60
+ # Build the same context and AAD that the field type would use
61
+ context = "TestModel:secret:#{loaded_model.identifier}"
62
+ puts "Context: #{context}"
63
+
64
+ # Build AAD using the same logic as EncryptedFieldType#build_aad
65
+ aad = loaded_model.exists? ? loaded_model.identifier : nil
66
+ puts "AAD: #{aad.inspect}"
67
+
68
+ puts "\n=== MANUAL FAMILIA::ENCRYPTION CALL ==="
69
+ begin
70
+ manual_decrypted = Familia::Encryption.decrypt(raw_encrypted, context: context, additional_data: aad)
71
+ puts "Manual decrypt result: #{manual_decrypted}"
72
+ rescue => e
73
+ puts "Manual decrypt ERROR: #{e.class}: #{e.message}"
74
+ end
@@ -0,0 +1,73 @@
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 "Testing cross-context validation with fresh encryption after AAD fix..."
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 FreshModelA < Familia::Horreum
16
+ feature :encrypted_fields
17
+ identifier_field :id
18
+ field :id
19
+ encrypted_field :api_key
20
+ end
21
+
22
+ class FreshModelB < 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 = FreshModelA.new(id: 'same-id')
33
+ model_b = FreshModelB.new(id: 'same-id')
34
+
35
+ puts "=== Fresh Encryption Test ==="
36
+ model_a.api_key = 'secret-key'
37
+ model_b.api_key = 'secret-key'
38
+
39
+ cipher_a = model_a.instance_variable_get(:@api_key)
40
+ cipher_b = model_b.instance_variable_get(:@api_key)
41
+
42
+ puts "cipher_a encrypted: #{cipher_a.encrypted_value}"
43
+ puts "cipher_b encrypted: #{cipher_b.encrypted_value}"
44
+
45
+ # Now try cross-context access
46
+ puts "\n=== Cross-context test ==="
47
+ model_a.instance_variable_set(:@api_key, cipher_b)
48
+ puts "Set cipher_b into model_a"
49
+
50
+ begin
51
+ result = model_a.api_key
52
+ puts "Got result: #{result.class}"
53
+
54
+ # Try to reveal it - this should fail now
55
+ result.reveal do |plaintext|
56
+ puts "ERROR: Successfully revealed: #{plaintext} - should have failed!"
57
+ end
58
+ rescue Familia::EncryptionError => e
59
+ puts "SUCCESS: Got expected encryption error: #{e.message}"
60
+ rescue => e
61
+ puts "Got unexpected error: #{e.class}: #{e.message}"
62
+ end
63
+
64
+ puts "\n=== Same-context test (should work) ==="
65
+ begin
66
+ model_a.instance_variable_set(:@api_key, cipher_a) # Back to original
67
+ result = model_a.api_key
68
+ result.reveal do |plaintext|
69
+ puts "SUCCESS: Same-context decryption worked: #{plaintext}"
70
+ end
71
+ rescue => e
72
+ puts "ERROR: Same-context failed: #{e.class}: #{e.message}"
73
+ end