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,93 @@
1
+ # try/data_types/counter_try.rb
2
+
3
+ require_relative '../helpers/test_helpers'
4
+
5
+ @a = Bone.new(token: 'atoken3')
6
+
7
+ ## Bone#dbkey
8
+ @a.dbkey
9
+ #=> 'bone:atoken3:object'
10
+
11
+ ## Familia::Counter should have default value of 0
12
+ @a.counter.value
13
+ #=> 0
14
+
15
+ ## Familia::Counter#value=
16
+ @a.counter.value = 42
17
+ #=> 42
18
+
19
+ ## Familia::Counter#to_i
20
+ @a.counter.to_i
21
+ #=> 42
22
+
23
+ ## Familia::Counter#to_s
24
+ @a.counter.to_s
25
+ #=> '42'
26
+
27
+ ## Familia::Counter#increment
28
+ @a.counter.increment
29
+ #=> 43
30
+
31
+ ## Familia::Counter#incrementby
32
+ @a.counter.incrementby(10)
33
+ #=> 53
34
+
35
+ ## Familia::Counter#decrement
36
+ @a.counter.decrement
37
+ #=> 52
38
+
39
+ ## Familia::Counter#decrementby
40
+ @a.counter.decrementby(5)
41
+ #=> 47
42
+
43
+ ## Familia::Counter#reset with value
44
+ @a.counter.reset(100)
45
+ #=> true
46
+
47
+ ## Familia::Counter#reset without value (defaults to 0)
48
+ @a.counter.reset
49
+ @a.counter.reset
50
+ @a.counter.value
51
+ #=> 0
52
+
53
+ ## Familia::Counter#atomic_increment_and_get
54
+ @a.counter.atomic_increment_and_get(25)
55
+ #=> 25
56
+
57
+ ## Familia::Counter#increment_if_less_than (success case)
58
+ @a.counter.increment_if_less_than(50, 10)
59
+ #=> true
60
+
61
+ ## Familia::Counter#value after conditional increment
62
+ @a.counter.to_i
63
+ #=> 35
64
+
65
+ ## Familia::Counter#increment_if_less_than (failure case)
66
+ @a.counter.increment_if_less_than(30, 10)
67
+ #=> false
68
+
69
+ ## Familia::Counter#value unchanged after failed conditional increment
70
+ @a.counter.to_i
71
+ #=> 35
72
+
73
+ ## Familia::Counter.new standalone
74
+ @counter = Familia::Counter.new 'test:counter'
75
+ @counter.dbkey
76
+ #=> 'test:counter'
77
+
78
+ ## Standalone counter starts at 0
79
+ @counter.value
80
+ #=> 0
81
+
82
+ ## Standalone counter increment
83
+ @counter.increment
84
+ #=> 1
85
+
86
+ ## Standalone counter set string value gets coerced to integer
87
+ @counter.value = "123"
88
+ @counter.to_i
89
+ #=> 123
90
+
91
+ # Cleanup
92
+ @a.counter.delete!
93
+ @counter.delete!
@@ -0,0 +1,133 @@
1
+ # try/data_types/lock_try.rb
2
+
3
+ require_relative '../helpers/test_helpers'
4
+
5
+ @a = Bone.new(token: 'atoken4')
6
+
7
+ ## Bone#dbkey
8
+ @a.dbkey
9
+ #=> 'bone:atoken4:object'
10
+
11
+ ## Familia::Lock should start unlocked
12
+ @a.lock.locked?
13
+ #=> false
14
+
15
+ ## Familia::Lock#value should be nil when unlocked
16
+ @a.lock.value
17
+ #=> nil
18
+
19
+ ## Familia::Lock#acquire returns token when successful
20
+ @token1 = @a.lock.acquire
21
+ @token1.class
22
+ #=> String
23
+
24
+ ## Familia::Lock#locked? after acquire
25
+ @a.lock.locked?
26
+ #=> true
27
+
28
+ ## Familia::Lock#held_by? with correct token
29
+ @a.lock.held_by?(@token1)
30
+ #=> true
31
+
32
+ ## Familia::Lock#held_by? with wrong token
33
+ @a.lock.held_by?('wrong-token')
34
+ #=> false
35
+
36
+ ## Familia::Lock#acquire when already locked returns false
37
+ @a.lock.acquire
38
+ #=> false
39
+
40
+ ## Familia::Lock#release with correct token
41
+ @a.lock.release(@token1)
42
+ #=> true
43
+
44
+ ## Familia::Lock#locked? after release
45
+ @a.lock.locked?
46
+ #=> false
47
+
48
+ ## Familia::Lock#release with wrong token (lock not held)
49
+ @a.lock.release('wrong-token')
50
+ #=> false
51
+
52
+ ## Familia::Lock#acquire with custom token
53
+ @custom_token = 'my-custom-token-123'
54
+ @result = @a.lock.acquire(@custom_token)
55
+ @result
56
+ #=> 'my-custom-token-123'
57
+
58
+ ## Familia::Lock#held_by? with custom token
59
+ @a.lock.held_by?(@custom_token)
60
+ #=> true
61
+
62
+ ## Familia::Lock#force_unlock!
63
+ @a.lock.force_unlock!
64
+ #=> true
65
+
66
+ ## Familia::Lock#locked? after force unlock
67
+ @a.lock.locked?
68
+ #=> false
69
+
70
+ ## Familia::Lock.new standalone
71
+ @lock = Familia::Lock.new 'test:lock'
72
+ @lock.dbkey
73
+ #=> 'test:lock'
74
+
75
+ ## Standalone lock starts unlocked
76
+ @lock.locked?
77
+ #=> false
78
+
79
+ ## Standalone lock acquire
80
+ @standalone_token = @lock.acquire
81
+ @standalone_token.class
82
+ #=> String
83
+
84
+ ## Standalone lock is now locked
85
+ @lock.locked?
86
+ #=> true
87
+
88
+ ## Standalone lock acquire with TTL
89
+ @lock.force_unlock!
90
+ @ttl_token = @lock.acquire('ttl-token', ttl: 1)
91
+ @ttl_token
92
+ #=> 'ttl-token'
93
+
94
+ ## Wait for TTL expiration and check if lock auto-expires
95
+ # Note: This test might be flaky in fast test runs
96
+ sleep 2
97
+ @lock.locked?
98
+ #=> false
99
+
100
+ ## Acquire with zero TTL should return false
101
+ @lock2 = Familia::Lock.new 'test:lock2'
102
+ @lock2.acquire('zero-ttl', ttl: 0)
103
+ #=> false
104
+
105
+ ## Lock should not be held after zero TTL rejection
106
+ @lock2.locked?
107
+ #=> false
108
+
109
+ ## Acquire with negative TTL should return false
110
+ @lock2.acquire('neg-ttl', ttl: -5)
111
+ #=> false
112
+
113
+ ## Lock should not be held after negative TTL rejection
114
+ @lock2.locked?
115
+ #=> false
116
+
117
+ ## Acquire with nil TTL should work (no expiration)
118
+ @nil_ttl_token = @lock2.acquire('no-expiry', ttl: nil)
119
+ @nil_ttl_token
120
+ #=> 'no-expiry'
121
+
122
+ ## Lock with nil TTL should be held
123
+ @lock2.locked?
124
+ #=> true
125
+
126
+ ## Lock with nil TTL should not have expiration
127
+ @lock2.current_expiration
128
+ #=> -1
129
+
130
+ ## Cleanup
131
+ @a.lock.delete!
132
+ @lock.delete!
133
+ @lock2.delete!
@@ -0,0 +1,82 @@
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 AAD during encrypt/decrypt process..."
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 DebugModelA < Familia::Horreum
16
+ feature :encrypted_fields
17
+ identifier_field :id
18
+ field :id
19
+ encrypted_field :api_key
20
+ end
21
+
22
+ class DebugModelB < Familia::Horreum
23
+ feature :encrypted_fields
24
+ identifier_field :id
25
+ field :id
26
+ encrypted_field :api_key
27
+ end
28
+
29
+ # Patch the EncryptedFieldType to add debug output
30
+ class Familia::EncryptedFieldType
31
+ alias_method :original_encrypt_value, :encrypt_value
32
+ alias_method :original_decrypt_value, :decrypt_value
33
+ alias_method :original_build_aad, :build_aad
34
+
35
+ def encrypt_value(record, value)
36
+ context = build_context(record)
37
+ aad = build_aad(record)
38
+ puts "[ENCRYPT] Class: #{record.class}, ID: #{record.identifier}, Context: #{context}, AAD: #{aad}"
39
+ original_encrypt_value(record, value)
40
+ end
41
+
42
+ def decrypt_value(record, encrypted)
43
+ context = build_context(record)
44
+ aad = build_aad(record)
45
+ puts "[DECRYPT] Class: #{record.class}, ID: #{record.identifier}, Context: #{context}, AAD: #{aad}"
46
+ original_decrypt_value(record, encrypted)
47
+ end
48
+
49
+ def build_aad(record)
50
+ aad = original_build_aad(record)
51
+ puts "[BUILD_AAD] Class: #{record.class}, ID: #{record.identifier}, AAD: #{aad}"
52
+ aad
53
+ end
54
+ end
55
+
56
+ # Clean database
57
+ Familia.dbclient.flushdb
58
+
59
+ model_a = DebugModelA.new(id: 'same-id')
60
+ model_b = DebugModelB.new(id: 'same-id')
61
+
62
+ puts "\n=== ENCRYPTION PHASE ==="
63
+ puts "Encrypting for ModelA:"
64
+ model_a.api_key = 'secret-key'
65
+ cipher_a = model_a.instance_variable_get(:@api_key)
66
+
67
+ puts "\nEncrypting for ModelB:"
68
+ model_b.api_key = 'secret-key'
69
+ cipher_b = model_b.instance_variable_get(:@api_key)
70
+
71
+ puts "\n=== DECRYPTION PHASE - Same Context ==="
72
+ puts "Decrypting ModelA with ModelA context:"
73
+ model_a.api_key.reveal { |plain| puts "Result: #{plain}" }
74
+
75
+ puts "\n=== DECRYPTION PHASE - Cross Context ==="
76
+ puts "Setting ModelB cipher into ModelA and trying to decrypt:"
77
+ model_a.instance_variable_set(:@api_key, cipher_b)
78
+ begin
79
+ model_a.api_key.reveal { |plain| puts "ERROR: Cross-context worked: #{plain}" }
80
+ rescue => e
81
+ puts "SUCCESS: Cross-context failed as expected: #{e.class}: #{e.message}"
82
+ end
@@ -0,0 +1,59 @@
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 internal call chain..."
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 INTERNAL DEBUGGING ==="
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
+ concealed_string = loaded_model.secret
34
+
35
+ # Access internal ConcealedString components
36
+ puts "ConcealedString @encrypted_data: #{concealed_string.instance_variable_get(:@encrypted_data)}"
37
+ puts "ConcealedString @record: #{concealed_string.instance_variable_get(:@record)}"
38
+ puts "ConcealedString @field_type: #{concealed_string.instance_variable_get(:@field_type)}"
39
+
40
+ record = concealed_string.instance_variable_get(:@record)
41
+ field_type = concealed_string.instance_variable_get(:@field_type)
42
+ encrypted_data = concealed_string.instance_variable_get(:@encrypted_data)
43
+
44
+ puts "\n=== STEP BY STEP DECRYPTION ==="
45
+ puts "Record class: #{record.class}"
46
+ puts "Record identifier: #{record.identifier}"
47
+ puts "Record exists?: #{record.exists?}"
48
+ puts "Field type class: #{field_type.class}"
49
+
50
+ # Test the exact same call that ConcealedString.reveal makes
51
+ puts "\nCalling field_type.decrypt_value(record, encrypted_data)..."
52
+ begin
53
+ result = field_type.decrypt_value(record, encrypted_data)
54
+ puts "decrypt_value result: #{result}"
55
+ puts "Result class: #{result.class}"
56
+ rescue => e
57
+ puts "decrypt_value ERROR: #{e.class}: #{e.message}"
58
+ puts e.backtrace.first(10)
59
+ end
@@ -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