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,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
|
@@ -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"
|
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?("
|
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?("
|
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?("
|
180
|
+
e.message.include?("Invalid Base64 encoding")
|
179
181
|
end
|
180
182
|
#=> true
|
181
183
|
|