familia 2.0.0.pre5 → 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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +8 -5
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +4 -3
  5. data/docs/wiki/API-Reference.md +95 -18
  6. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  7. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  8. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  9. data/docs/wiki/Feature-System-Guide.md +600 -0
  10. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  11. data/docs/wiki/Field-System-Guide.md +784 -0
  12. data/docs/wiki/Home.md +72 -15
  13. data/docs/wiki/Implementation-Guide.md +126 -33
  14. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  15. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  16. data/docs/wiki/Security-Model.md +65 -25
  17. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  18. data/lib/familia/base.rb +1 -1
  19. data/lib/familia/data_type/types/counter.rb +38 -0
  20. data/lib/familia/data_type/types/hashkey.rb +18 -0
  21. data/lib/familia/data_type/types/lock.rb +43 -0
  22. data/lib/familia/data_type/types/string.rb +9 -2
  23. data/lib/familia/data_type.rb +2 -2
  24. data/lib/familia/encryption/encrypted_data.rb +137 -0
  25. data/lib/familia/encryption/manager.rb +21 -4
  26. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  27. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  28. data/lib/familia/encryption.rb +1 -1
  29. data/lib/familia/errors.rb +17 -3
  30. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  31. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  32. data/lib/familia/features/expiration.rb +1 -1
  33. data/lib/familia/features/quantization.rb +1 -1
  34. data/lib/familia/features/safe_dump.rb +1 -1
  35. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  36. data/lib/familia/features/transient_fields.rb +1 -1
  37. data/lib/familia/field_type.rb +5 -2
  38. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  39. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  40. data/lib/familia/horreum/core/serialization.rb +535 -0
  41. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  42. data/lib/familia/horreum/core.rb +21 -0
  43. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  44. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +44 -28
  45. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  46. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  47. data/lib/familia/horreum.rb +17 -17
  48. data/lib/familia/version.rb +1 -1
  49. data/lib/familia.rb +1 -1
  50. data/try/core/create_method_try.rb +240 -0
  51. data/try/core/database_consistency_try.rb +299 -0
  52. data/try/core/errors_try.rb +25 -4
  53. data/try/core/familia_try.rb +1 -1
  54. data/try/core/persistence_operations_try.rb +297 -0
  55. data/try/data_types/counter_try.rb +93 -0
  56. data/try/data_types/lock_try.rb +133 -0
  57. data/try/debugging/debug_aad_process.rb +82 -0
  58. data/try/debugging/debug_concealed_internal.rb +59 -0
  59. data/try/debugging/debug_concealed_reveal.rb +61 -0
  60. data/try/debugging/debug_context_aad.rb +68 -0
  61. data/try/debugging/debug_context_simple.rb +80 -0
  62. data/try/debugging/debug_cross_context.rb +62 -0
  63. data/try/debugging/debug_database_load.rb +64 -0
  64. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  65. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  66. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  67. data/try/debugging/debug_field_decrypt.rb +74 -0
  68. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  69. data/try/debugging/debug_load_path.rb +66 -0
  70. data/try/debugging/debug_method_definition.rb +46 -0
  71. data/try/debugging/debug_method_resolution.rb +41 -0
  72. data/try/debugging/debug_minimal.rb +24 -0
  73. data/try/debugging/debug_provider.rb +68 -0
  74. data/try/debugging/debug_secure_behavior.rb +73 -0
  75. data/try/debugging/debug_string_class.rb +46 -0
  76. data/try/debugging/debug_test.rb +46 -0
  77. data/try/debugging/debug_test_design.rb +80 -0
  78. data/try/encryption/encryption_core_try.rb +3 -3
  79. data/try/features/encrypted_fields_core_try.rb +19 -11
  80. data/try/features/encrypted_fields_integration_try.rb +66 -70
  81. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  82. data/try/features/encrypted_fields_security_try.rb +151 -144
  83. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  84. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  85. data/try/features/encryption_fields/context_isolation_try.rb +29 -8
  86. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  87. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  88. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  89. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  90. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  91. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  92. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  93. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  94. data/try/features/feature_dependencies_try.rb +3 -3
  95. data/try/features/transient_fields_core_try.rb +1 -1
  96. data/try/features/transient_fields_integration_try.rb +1 -1
  97. data/try/helpers/test_helpers.rb +25 -0
  98. data/try/horreum/enhanced_conflict_handling_try.rb +1 -1
  99. data/try/horreum/initialization_try.rb +1 -1
  100. data/try/horreum/relations_try.rb +1 -1
  101. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  102. data/try/horreum/serialization_try.rb +39 -4
  103. data/try/models/customer_safe_dump_try.rb +1 -1
  104. data/try/models/customer_try.rb +1 -1
  105. metadata +51 -10
  106. data/TEST_COVERAGE.md +0 -40
  107. data/lib/familia/horreum/serialization.rb +0 -473
@@ -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
@@ -0,0 +1,66 @@
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 load path and setter behavior..."
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=== TRACING THE SETTER DURING LOAD ==="
25
+
26
+ # First, let's monkey-patch the setter to see what's being called
27
+ original_setter = TestModel.instance_method(:secret=)
28
+ TestModel.define_method(:secret=) do |value|
29
+ puts "\n--- SETTER CALLED ---"
30
+ puts "Setting secret to: #{value}"
31
+ puts "Value class: #{value.class}"
32
+ puts "Record exists?: #{exists?}"
33
+ puts "Record identifier: #{identifier}"
34
+
35
+ if value.is_a?(String) && !value.nil?
36
+ puts "Checking if value is encrypted JSON..."
37
+ field_type = TestModel.field_types[:secret]
38
+ is_encrypted = field_type.encrypted_json?(value)
39
+ puts "encrypted_json? result: #{is_encrypted}"
40
+
41
+ if is_encrypted
42
+ puts "Path: Creating ConcealedString WITHOUT re-encryption"
43
+ else
44
+ puts "Path: Will ENCRYPT value and create ConcealedString"
45
+ end
46
+ end
47
+ puts "--- END SETTER ---\n"
48
+
49
+ # Call original setter
50
+ original_setter.bind(self).call(value)
51
+ end
52
+
53
+ puts "\nCreating and saving model..."
54
+ model = TestModel.new(id: 'test1')
55
+ puts "Setting secret on in-memory model..."
56
+ model.secret = 'plaintext-secret'
57
+
58
+ puts "\nSaving model..."
59
+ model.save
60
+
61
+ puts "\nRaw data in database:"
62
+ raw_data = Familia.dbclient.hget('testmodel:test1:object', 'secret')
63
+ puts "Raw encrypted in DB: #{raw_data[0..100]}..." # Truncate for readability
64
+
65
+ puts "\n=== LOADING MODEL ==="
66
+ loaded_model = TestModel.load('test1')
@@ -0,0 +1,46 @@
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 "Checking method definition..."
8
+
9
+ field_type = Familia::EncryptedFieldType.new(:test)
10
+ puts "Method source location: #{field_type.method(:encrypted_json?).source_location}"
11
+
12
+ # Check if the method exists
13
+ puts "Method exists?: #{field_type.respond_to?(:encrypted_json?)}"
14
+
15
+ # Get the method object and examine it
16
+ method_obj = field_type.method(:encrypted_json?)
17
+ puts "Method object: #{method_obj}"
18
+ puts "Method owner: #{method_obj.owner}"
19
+
20
+ # Call with debugging
21
+ puts "\nCalling method..."
22
+ result = method_obj.call('{"algorithm":"test"}')
23
+ puts "Result: #{result}"
24
+
25
+ # Let's also check the class directly
26
+ puts "\nChecking class method..."
27
+ class_result = Familia::EncryptedFieldType.new(:test2).encrypted_json?('{"algorithm":"test"}')
28
+ puts "Class result: #{class_result}"
29
+
30
+ # Try to define the method inline to see if it works
31
+ puts "\nDefining method inline..."
32
+ field_type.define_singleton_method(:test_encrypted_json?) do |data|
33
+ puts "Inline method called with: #{data}"
34
+ begin
35
+ parsed = JSON.parse(data)
36
+ result = parsed.is_a?(Hash) && parsed.key?('algorithm')
37
+ puts "Inline result: #{result}"
38
+ return result
39
+ rescue => e
40
+ puts "Inline error: #{e}"
41
+ return false
42
+ end
43
+ end
44
+
45
+ inline_result = field_type.test_encrypted_json?('{"algorithm":"test"}')
46
+ puts "Inline method result: #{inline_result}"
@@ -0,0 +1,41 @@
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 method resolution for encrypted_json?..."
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
+ encrypted_json = '{"algorithm":"xchacha20poly1305","nonce":"test","ciphertext":"test","auth_tag":"test","key_version":"v1"}'
23
+
24
+ puts "Field type class: #{field_type.class}"
25
+ puts "Field type method location: #{field_type.method(:encrypted_json?).source_location}"
26
+
27
+ # Test the method directly
28
+ puts "\nDirect method test:"
29
+ puts "Result: #{field_type.encrypted_json?(encrypted_json)}"
30
+
31
+ # Test with a fresh instance
32
+ puts "\nCreating fresh EncryptedFieldType instance:"
33
+ fresh_field_type = Familia::EncryptedFieldType.new(:test)
34
+ puts "Fresh field type method location: #{fresh_field_type.method(:encrypted_json?).source_location}"
35
+ puts "Fresh instance result: #{fresh_field_type.encrypted_json?(encrypted_json)}"
36
+
37
+ # Test with simple JSON
38
+ simple_json = '{"algorithm":"test"}'
39
+ puts "\nTesting with simple JSON: #{simple_json}"
40
+ puts "Field type result: #{field_type.encrypted_json?(simple_json)}"
41
+ puts "Fresh instance result: #{fresh_field_type.encrypted_json?(simple_json)}"
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
4
+ require 'familia'
5
+
6
+ # Minimal test
7
+ field_type = Familia::EncryptedFieldType.new(:test)
8
+ json = '{"algorithm":"test"}'
9
+
10
+ puts "Testing: #{json}"
11
+ puts "Result: #{field_type.encrypted_json?(json)}"
12
+
13
+ # Manual implementation
14
+ puts "\nManual check:"
15
+ begin
16
+ parsed = JSON.parse(json)
17
+ puts "Parsed: #{parsed}"
18
+ puts "Is hash?: #{parsed.is_a?(Hash)}"
19
+ puts "Has algorithm?: #{parsed.key?('algorithm')}"
20
+ result = parsed.is_a?(Hash) && parsed.key?('algorithm')
21
+ puts "Manual result: #{result}"
22
+ rescue => e
23
+ puts "Error: #{e}"
24
+ end
@@ -0,0 +1,68 @@
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 provider-level decryption..."
9
+
10
+ # Setup
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
+ # Simulate the conditions during encryption vs decryption
16
+ class TestModel < Familia::Horreum
17
+ feature :encrypted_fields
18
+ identifier_field :id
19
+ field :id
20
+ encrypted_field :secret
21
+ end
22
+
23
+ # Clean database
24
+ Familia.dbclient.flushdb
25
+
26
+ puts "\n=== DIRECT ENCRYPTION/DECRYPTION TEST ==="
27
+
28
+ # Test direct encryption/decryption with different AAD values
29
+ plaintext = "test-secret"
30
+ context = "TestModel:secret:test1"
31
+
32
+ # Scenario 1: Encrypt with AAD = nil (before save)
33
+ puts "Encrypting with AAD = nil..."
34
+ encrypted_json_1 = Familia::Encryption.encrypt(plaintext, context: context, additional_data: nil)
35
+ puts "Encrypted: #{encrypted_json_1}"
36
+
37
+ # Try to decrypt with AAD = nil
38
+ puts "Decrypting with AAD = nil..."
39
+ decrypted_1 = Familia::Encryption.decrypt(encrypted_json_1, context: context, additional_data: nil)
40
+ puts "Decrypted: #{decrypted_1}"
41
+
42
+ # Try to decrypt with AAD = "test1" (what happens after load)
43
+ puts "Decrypting with AAD = 'test1'..."
44
+ begin
45
+ decrypted_2 = Familia::Encryption.decrypt(encrypted_json_1, context: context, additional_data: "test1")
46
+ puts "Decrypted: #{decrypted_2}"
47
+ rescue => e
48
+ puts "Decryption failed: #{e.class}: #{e.message}"
49
+ end
50
+
51
+ # Scenario 2: Encrypt with AAD = "test1" (after save)
52
+ puts "\nEncrypting with AAD = 'test1'..."
53
+ encrypted_json_2 = Familia::Encryption.encrypt(plaintext, context: context, additional_data: "test1")
54
+ puts "Encrypted: #{encrypted_json_2}"
55
+
56
+ # Try to decrypt with AAD = "test1"
57
+ puts "Decrypting with AAD = 'test1'..."
58
+ decrypted_3 = Familia::Encryption.decrypt(encrypted_json_2, context: context, additional_data: "test1")
59
+ puts "Decrypted: #{decrypted_3}"
60
+
61
+ # Try to decrypt with AAD = nil
62
+ puts "Decrypting with AAD = nil..."
63
+ begin
64
+ decrypted_4 = Familia::Encryption.decrypt(encrypted_json_2, context: context, additional_data: nil)
65
+ puts "Decrypted: #{decrypted_4}"
66
+ rescue => e
67
+ puts "Decryption failed: #{e.class}: #{e.message}"
68
+ 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 "Debugging secure_by_default_behavior test failure..."
9
+
10
+ # Setup encryption keys for testing
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 DebugSecureUserAccount < Familia::Horreum
16
+ feature :encrypted_fields
17
+ identifier_field :id
18
+ field :id
19
+ field :username
20
+ field :email
21
+ encrypted_field :password_hash
22
+ encrypted_field :api_secret
23
+ encrypted_field :recovery_key
24
+ end
25
+
26
+ # Patch for debugging
27
+ class Familia::EncryptedFieldType
28
+ alias_method :original_encrypt_value, :encrypt_value
29
+ alias_method :original_decrypt_value, :decrypt_value
30
+
31
+ def encrypt_value(record, value)
32
+ aad = build_aad(record)
33
+ puts "[ENCRYPT] #{record.class}##{@name}: exists=#{record.exists?}, AAD=#{aad}"
34
+ original_encrypt_value(record, value)
35
+ end
36
+
37
+ def decrypt_value(record, encrypted)
38
+ aad = build_aad(record)
39
+ puts "[DECRYPT] #{record.class}##{@name}: exists=#{record.exists?}, AAD=#{aad}"
40
+ original_decrypt_value(record, encrypted)
41
+ end
42
+ end
43
+
44
+ # Clean database
45
+ Familia.dbclient.flushdb
46
+
47
+ puts "\n=== CREATION AND ENCRYPTION ==="
48
+ user = DebugSecureUserAccount.new(id: "user123")
49
+ puts "After new: exists=#{user.exists?}"
50
+
51
+ user.username = "john_doe"
52
+ user.email = "john@example.com"
53
+ user.password_hash = "bcrypt$2a$12$abcdef..."
54
+ user.api_secret = "secret-api-key-12345"
55
+
56
+ puts "\n=== SAVE TO DATABASE ==="
57
+ result = user.save
58
+ puts "Save result: #{result}"
59
+ puts "After save: exists=#{user.exists?}"
60
+
61
+ puts "\n=== FRESH LOAD FROM DATABASE ==="
62
+ fresh_user = DebugSecureUserAccount.load("user123")
63
+ puts "Fresh user loaded: exists=#{fresh_user.exists?}"
64
+
65
+ puts "\n=== DECRYPTION ATTEMPT ==="
66
+ begin
67
+ fresh_user.password_hash.reveal do |plaintext|
68
+ puts "SUCCESS: Decrypted password_hash: #{plaintext}"
69
+ end
70
+ rescue => e
71
+ puts "ERROR: #{e.class}: #{e.message}"
72
+ puts "This suggests AAD mismatch between save and load"
73
+ end
@@ -0,0 +1,46 @@
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 "Investigating the string class issue..."
8
+
9
+ field_type = Familia::EncryptedFieldType.new(:test)
10
+ json = '{"algorithm":"test"}'
11
+
12
+ puts "Testing string object:"
13
+ puts "json: #{json}"
14
+ puts "json.class: #{json.class}"
15
+ puts "json.is_a?(String): #{json.is_a?(String)}"
16
+ puts "json.kind_of?(String): #{json.kind_of?(String)}"
17
+ puts "json.class == String: #{json.class == String}"
18
+ puts "json.class.ancestors: #{json.class.ancestors}"
19
+
20
+ # Let's test with various string types
21
+ frozen_string = '{"algorithm":"test"}'.freeze
22
+ puts "\nTesting frozen string:"
23
+ puts "frozen.class: #{frozen_string.class}"
24
+ puts "frozen.is_a?(String): #{frozen_string.is_a?(String)}"
25
+
26
+ # Test with different string creation methods
27
+ string_new = String.new('{"algorithm":"test"}')
28
+ puts "\nTesting String.new:"
29
+ puts "string_new.class: #{string_new.class}"
30
+ puts "string_new.is_a?(String): #{string_new.is_a?(String)}"
31
+
32
+ # Let's inspect what the method actually receives
33
+ puts "\nTesting what the method actually receives..."
34
+ field_type.define_singleton_method(:debug_encrypted_json?) do |data|
35
+ puts "Received data: #{data.inspect}"
36
+ puts "Data class: #{data.class}"
37
+ puts "Data class ancestors: #{data.class.ancestors}"
38
+ puts "Is String?: #{data.is_a?(String)}"
39
+ puts "Kind of String?: #{data.kind_of?(String)}"
40
+ puts "Equals String class?: #{data.class == String}"
41
+ puts "Responds to string methods?: #{data.respond_to?(:upcase)}"
42
+ return "debug_complete"
43
+ end
44
+
45
+ result = field_type.debug_encrypted_json?(json)
46
+ puts "Debug result: #{result}"