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,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}"
@@ -0,0 +1,46 @@
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_for_testing 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
+ Familia.config.encryption_keys = { v1: SecureRandom.hex(32) }
18
+ Familia.config.current_key_version = :v1
19
+
20
+ model = TestModel.new(id: 'test1')
21
+ model.secret = 'plaintext-secret'
22
+
23
+ puts "Setting secret to: plaintext-secret"
24
+ puts "Class of secret field: #{model.secret.class}"
25
+ puts "Secret field value: #{model.secret}"
26
+
27
+ if model.secret.respond_to?(:reveal_for_testing)
28
+ puts "Attempting reveal_for_testing..."
29
+ begin
30
+ decrypted = model.secret.reveal_for_testing
31
+ puts "reveal_for_testing result: #{decrypted}"
32
+ rescue => e
33
+ puts "reveal_for_testing failed: #{e.class}: #{e.message}"
34
+ end
35
+ end
36
+
37
+ if model.secret.respond_to?(:reveal)
38
+ puts "Attempting reveal block..."
39
+ begin
40
+ model.secret.reveal do |decrypted|
41
+ puts "reveal block result: #{decrypted}"
42
+ end
43
+ rescue => e
44
+ puts "reveal block failed: #{e.class}: #{e.message}"
45
+ end
46
+ end
@@ -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
@@ -61,6 +61,7 @@ end
61
61
  @symbol_result[:name]
62
62
  #=> "John"
63
63
 
64
+ ## String keys also work correctly
64
65
  @string_result['name']
65
66
  #=> "John"
66
67
 
@@ -6,6 +6,7 @@ require_relative '../helpers/test_helpers'
6
6
  TestClass = Class.new(Familia::Horreum) do
7
7
  identifier_field :email
8
8
  field :email
9
+ field :ttl # This should cause an error
9
10
  field :default_expiration
10
11
  end
11
12
 
@@ -39,6 +39,7 @@ end
39
39
  #=:> BadIdentifierTest
40
40
 
41
41
  # Test polymorphic string usage for Familia objects
42
+ ## Customer creation with string coercion
42
43
  @customer = Customer.new(@customer_id)
43
44
  @customer.name = 'John Doe'
44
45
  @customer.planid = 'premium'
@@ -86,6 +87,7 @@ session
86
87
  @session.to_s
87
88
  #=<> @session_id
88
89
 
90
+ ## Test lookup by ID functionality
89
91
  lookup_by_id(@customer)
90
92
  #=> @customer_id.upcase
91
93
 
@@ -40,7 +40,9 @@ Familia.config.current_key_version = :v2
40
40
  encrypted = Familia::Encryption.encrypt(plaintext, context: context)
41
41
  encrypted_data = JSON.parse(encrypted, symbolize_names: true)
42
42
  encrypted_data[:key_version]
43
- #=> "v2"## Nonce is unique - same plaintext encrypts to different ciphertext
43
+ #=> "v2"
44
+
45
+ ## Nonce is unique - same plaintext encrypts to different ciphertext
44
46
  test_keys = { v1: Base64.strict_encode64('a' * 32), v2: Base64.strict_encode64('b' * 32) }
45
47
  context = "TestModel:secret_field:user123"
46
48
  plaintext = "sensitive data here"
@@ -129,7 +131,7 @@ Familia.config.current_key_version = :v1
129
131
 
130
132
  Familia::Encryption.decrypt("invalid json {", context: context)
131
133
  #=!> Familia::EncryptionError
132
- #==> error.message.include?("Decryption failed")
134
+ #==> error.message.include?("Invalid JSON structure")
133
135
 
134
136
  ## Invalid base64 nonce raises sanitized error
135
137
  test_keys = { v1: Base64.strict_encode64('a' * 32) }
@@ -151,7 +153,7 @@ begin
151
153
  Familia::Encryption.decrypt(invalid_encrypted, context: context)
152
154
  "should_not_reach_here"
153
155
  rescue Familia::EncryptionError => e
154
- e.message.include?("Decryption failed")
156
+ e.message.include?("Invalid Base64 encoding")
155
157
  end
156
158
  #=> true
157
159
 
@@ -175,7 +177,7 @@ begin
175
177
  Familia::Encryption.decrypt(invalid_encrypted, context: context)
176
178
  "should_not_reach_here"
177
179
  rescue Familia::EncryptionError => e
178
- e.message.include?("Decryption failed")
180
+ e.message.include?("Invalid Base64 encoding")
179
181
  end
180
182
  #=> true
181
183