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.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- 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
|