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.
- checksums.yaml +4 -4
- data/CLAUDE.md +8 -5
- data/Gemfile +1 -1
- 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 +600 -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 +72 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +1 -1
- 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 +2 -2
- 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 +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/expiration.rb +1 -1
- data/lib/familia/features/quantization.rb +1 -1
- 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 +1 -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} +44 -28
- 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/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/encryption/encryption_core_try.rb +3 -3
- 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 +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +29 -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/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +25 -0
- data/try/horreum/enhanced_conflict_handling_try.rb +1 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +1 -1
- 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
- metadata +51 -10
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/horreum/serialization.rb +0 -473
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true' # Mark as test environment
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Testing encrypted_json? method..."
|
9
|
+
|
10
|
+
class TestModel < Familia::Horreum
|
11
|
+
feature :encrypted_fields
|
12
|
+
identifier_field :id
|
13
|
+
field :id
|
14
|
+
encrypted_field :secret
|
15
|
+
end
|
16
|
+
|
17
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
18
|
+
Familia.config.encryption_keys = test_keys
|
19
|
+
Familia.config.current_key_version = :v1
|
20
|
+
|
21
|
+
field_type = TestModel.field_types[:secret]
|
22
|
+
|
23
|
+
# Test with actual encrypted JSON from the load path
|
24
|
+
encrypted_json = '{"algorithm":"xchacha20poly1305","nonce":"RDK0GSY3Vbrbv7OAgol10bHOmderAExt","ciphertext":"uo8j6Pm6tV68BcvqK5maXQ==","auth_tag":"5Cr1QgTnajnWIji0fsQP0g==","key_version":"v1"}'
|
25
|
+
|
26
|
+
puts "Testing encrypted JSON: #{encrypted_json[0..80]}..."
|
27
|
+
puts "String class: #{encrypted_json.class}"
|
28
|
+
puts "Is string?: #{encrypted_json.is_a?(String)}"
|
29
|
+
|
30
|
+
puts "\nManual JSON parsing:"
|
31
|
+
begin
|
32
|
+
parsed = JSON.parse(encrypted_json)
|
33
|
+
puts "Parsed successfully: #{parsed.class}"
|
34
|
+
puts "Is hash?: #{parsed.is_a?(Hash)}"
|
35
|
+
puts "Has algorithm key?: #{parsed.key?('algorithm')}"
|
36
|
+
puts "Algorithm value: #{parsed['algorithm']}"
|
37
|
+
rescue => e
|
38
|
+
puts "JSON parse error: #{e.class}: #{e.message}"
|
39
|
+
end
|
40
|
+
|
41
|
+
puts "\nTesting field_type.encrypted_json? method:"
|
42
|
+
result = field_type.encrypted_json?(encrypted_json)
|
43
|
+
puts "encrypted_json? result: #{result}"
|
44
|
+
|
45
|
+
puts "\nTesting with symbol keys:"
|
46
|
+
begin
|
47
|
+
parsed_sym = JSON.parse(encrypted_json, symbolize_names: true)
|
48
|
+
puts "Parsed with symbols: #{parsed_sym.class}"
|
49
|
+
puts "Has :algorithm key?: #{parsed_sym.key?(:algorithm)}"
|
50
|
+
puts "Has 'algorithm' key?: #{parsed_sym.key?('algorithm')}"
|
51
|
+
rescue => e
|
52
|
+
puts "Symbol parse error: #{e.class}: #{e.message}"
|
53
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true' # Mark as test environment
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Step-by-step debugging of encrypted_json? method..."
|
9
|
+
|
10
|
+
class TestModel < Familia::Horreum
|
11
|
+
feature :encrypted_fields
|
12
|
+
identifier_field :id
|
13
|
+
field :id
|
14
|
+
encrypted_field :secret
|
15
|
+
end
|
16
|
+
|
17
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
18
|
+
Familia.config.encryption_keys = test_keys
|
19
|
+
Familia.config.current_key_version = :v1
|
20
|
+
|
21
|
+
field_type = TestModel.field_types[:secret]
|
22
|
+
|
23
|
+
# Monkey patch the encrypted_json? method to add debugging
|
24
|
+
class Familia::EncryptedFieldType
|
25
|
+
def encrypted_json?(data)
|
26
|
+
puts "\n=== encrypted_json? DEBUG ==="
|
27
|
+
puts "Input data: #{data}"
|
28
|
+
puts "Data class: #{data.class}"
|
29
|
+
puts "Is String?: #{data.is_a?(String)}"
|
30
|
+
|
31
|
+
return false unless data.is_a?(String)
|
32
|
+
puts "Passed String check"
|
33
|
+
|
34
|
+
begin
|
35
|
+
puts "Attempting JSON.parse..."
|
36
|
+
parsed = JSON.parse(data)
|
37
|
+
puts "Parsed result: #{parsed}"
|
38
|
+
puts "Parsed class: #{parsed.class}"
|
39
|
+
puts "Is Hash?: #{parsed.is_a?(Hash)}"
|
40
|
+
|
41
|
+
if parsed.is_a?(Hash)
|
42
|
+
puts "Hash keys: #{parsed.keys}"
|
43
|
+
puts "Has 'algorithm' key?: #{parsed.key?('algorithm')}"
|
44
|
+
result = parsed.key?('algorithm')
|
45
|
+
puts "Final result: #{result}"
|
46
|
+
return result
|
47
|
+
else
|
48
|
+
puts "Not a hash, returning false"
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
rescue JSON::ParserError => e
|
52
|
+
puts "JSON parse error: #{e.message}"
|
53
|
+
false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
encrypted_json = '{"algorithm":"xchacha20poly1305","nonce":"RDK0GSY3Vbrbv7OAgol10bHOmderAExt","ciphertext":"uo8j6Pm6tV68BcvqK5maXQ==","auth_tag":"5Cr1QgTnajnWIji0fsQP0g==","key_version":"v1"}'
|
59
|
+
|
60
|
+
puts "Calling field_type.encrypted_json?..."
|
61
|
+
result = field_type.encrypted_json?(encrypted_json)
|
62
|
+
puts "\nFINAL RESULT: #{result}"
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true'
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Investigating exists? lifecycle..."
|
9
|
+
|
10
|
+
class TestModel < Familia::Horreum
|
11
|
+
feature :encrypted_fields
|
12
|
+
identifier_field :id
|
13
|
+
field :id
|
14
|
+
encrypted_field :secret
|
15
|
+
end
|
16
|
+
|
17
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
18
|
+
Familia.config.encryption_keys = test_keys
|
19
|
+
Familia.config.current_key_version = :v1
|
20
|
+
|
21
|
+
# Clean database
|
22
|
+
Familia.dbclient.flushdb
|
23
|
+
|
24
|
+
# Override the exists? method to add logging
|
25
|
+
class TestModel
|
26
|
+
alias_method :original_exists?, :exists?
|
27
|
+
def exists?
|
28
|
+
result = original_exists?
|
29
|
+
puts "EXISTS? called - result: #{result} (identifier: #{identifier})" if ENV['TEST']
|
30
|
+
result
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
puts "\n=== CREATION AND SAVE ==="
|
35
|
+
model = TestModel.new(id: 'test1')
|
36
|
+
puts "After new - exists?: #{model.exists?}"
|
37
|
+
model.secret = 'plaintext-secret'
|
38
|
+
puts "After setting secret - exists?: #{model.exists?}"
|
39
|
+
model.save
|
40
|
+
puts "After save - exists?: #{model.exists?}"
|
41
|
+
|
42
|
+
puts "\n=== LOAD FROM DATABASE ==="
|
43
|
+
puts "About to load..."
|
44
|
+
loaded_model = TestModel.load('test1')
|
45
|
+
puts "After load - exists?: #{loaded_model.exists?}"
|
46
|
+
|
47
|
+
puts "\n=== TRYING TO ACCESS SECRET ==="
|
48
|
+
begin
|
49
|
+
loaded_model.secret.reveal do |plaintext|
|
50
|
+
puts "Successfully revealed: #{plaintext}"
|
51
|
+
end
|
52
|
+
rescue => e
|
53
|
+
puts "Failed to reveal: #{e.class}: #{e.message}"
|
54
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true' # Mark as test environment
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Testing field-level decryption path..."
|
9
|
+
|
10
|
+
class TestModel < Familia::Horreum
|
11
|
+
feature :encrypted_fields
|
12
|
+
identifier_field :id
|
13
|
+
field :id
|
14
|
+
encrypted_field :secret
|
15
|
+
end
|
16
|
+
|
17
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
18
|
+
Familia.config.encryption_keys = test_keys
|
19
|
+
Familia.config.current_key_version = :v1
|
20
|
+
|
21
|
+
# Clean database
|
22
|
+
Familia.dbclient.flushdb
|
23
|
+
|
24
|
+
puts "\n=== FIELD-LEVEL DECRYPTION PATH ==="
|
25
|
+
|
26
|
+
# Create and save model
|
27
|
+
model = TestModel.new(id: 'test1')
|
28
|
+
model.secret = 'plaintext-secret'
|
29
|
+
model.save
|
30
|
+
|
31
|
+
# Load model from database
|
32
|
+
loaded_model = TestModel.load('test1')
|
33
|
+
|
34
|
+
# Get the field type and test direct decryption
|
35
|
+
puts "Available field types: #{TestModel.field_types.inspect}"
|
36
|
+
secret_field_type = TestModel.field_types[:secret]
|
37
|
+
puts "Field type class: #{secret_field_type.class}"
|
38
|
+
|
39
|
+
# Get the raw encrypted data
|
40
|
+
raw_encrypted = Familia.dbclient.hget('testmodel:test1:object', 'secret')
|
41
|
+
puts "Raw encrypted from DB: #{raw_encrypted}"
|
42
|
+
|
43
|
+
puts "\n=== DIRECT FIELD TYPE DECRYPTION ==="
|
44
|
+
|
45
|
+
# Test the field type decrypt_value method directly
|
46
|
+
puts "Testing field_type.decrypt_value with loaded model..."
|
47
|
+
begin
|
48
|
+
direct_decrypted = secret_field_type.decrypt_value(loaded_model, raw_encrypted)
|
49
|
+
puts "Direct field decrypt result: #{direct_decrypted}"
|
50
|
+
puts "Result class: #{direct_decrypted.class}"
|
51
|
+
rescue => e
|
52
|
+
puts "Direct field decrypt ERROR: #{e.class}: #{e.message}"
|
53
|
+
puts e.backtrace.first(5)
|
54
|
+
end
|
55
|
+
|
56
|
+
puts "\n=== AAD INVESTIGATION ==="
|
57
|
+
puts "loaded_model.exists?: #{loaded_model.exists?}"
|
58
|
+
puts "loaded_model.identifier: #{loaded_model.identifier}"
|
59
|
+
|
60
|
+
# Build the same context and AAD that the field type would use
|
61
|
+
context = "TestModel:secret:#{loaded_model.identifier}"
|
62
|
+
puts "Context: #{context}"
|
63
|
+
|
64
|
+
# Build AAD using the same logic as EncryptedFieldType#build_aad
|
65
|
+
aad = loaded_model.exists? ? loaded_model.identifier : nil
|
66
|
+
puts "AAD: #{aad.inspect}"
|
67
|
+
|
68
|
+
puts "\n=== MANUAL FAMILIA::ENCRYPTION CALL ==="
|
69
|
+
begin
|
70
|
+
manual_decrypted = Familia::Encryption.decrypt(raw_encrypted, context: context, additional_data: aad)
|
71
|
+
puts "Manual decrypt result: #{manual_decrypted}"
|
72
|
+
rescue => e
|
73
|
+
puts "Manual decrypt ERROR: #{e.class}: #{e.message}"
|
74
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true'
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Testing cross-context validation with fresh encryption after AAD fix..."
|
9
|
+
|
10
|
+
# Setup encryption keys
|
11
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
12
|
+
Familia.config.encryption_keys = test_keys
|
13
|
+
Familia.config.current_key_version = :v1
|
14
|
+
|
15
|
+
class FreshModelA < Familia::Horreum
|
16
|
+
feature :encrypted_fields
|
17
|
+
identifier_field :id
|
18
|
+
field :id
|
19
|
+
encrypted_field :api_key
|
20
|
+
end
|
21
|
+
|
22
|
+
class FreshModelB < Familia::Horreum
|
23
|
+
feature :encrypted_fields
|
24
|
+
identifier_field :id
|
25
|
+
field :id
|
26
|
+
encrypted_field :api_key
|
27
|
+
end
|
28
|
+
|
29
|
+
# Clean database
|
30
|
+
Familia.dbclient.flushdb
|
31
|
+
|
32
|
+
model_a = FreshModelA.new(id: 'same-id')
|
33
|
+
model_b = FreshModelB.new(id: 'same-id')
|
34
|
+
|
35
|
+
puts "=== Fresh Encryption Test ==="
|
36
|
+
model_a.api_key = 'secret-key'
|
37
|
+
model_b.api_key = 'secret-key'
|
38
|
+
|
39
|
+
cipher_a = model_a.instance_variable_get(:@api_key)
|
40
|
+
cipher_b = model_b.instance_variable_get(:@api_key)
|
41
|
+
|
42
|
+
puts "cipher_a encrypted: #{cipher_a.encrypted_value}"
|
43
|
+
puts "cipher_b encrypted: #{cipher_b.encrypted_value}"
|
44
|
+
|
45
|
+
# Now try cross-context access
|
46
|
+
puts "\n=== Cross-context test ==="
|
47
|
+
model_a.instance_variable_set(:@api_key, cipher_b)
|
48
|
+
puts "Set cipher_b into model_a"
|
49
|
+
|
50
|
+
begin
|
51
|
+
result = model_a.api_key
|
52
|
+
puts "Got result: #{result.class}"
|
53
|
+
|
54
|
+
# Try to reveal it - this should fail now
|
55
|
+
result.reveal do |plaintext|
|
56
|
+
puts "ERROR: Successfully revealed: #{plaintext} - should have failed!"
|
57
|
+
end
|
58
|
+
rescue Familia::EncryptionError => e
|
59
|
+
puts "SUCCESS: Got expected encryption error: #{e.message}"
|
60
|
+
rescue => e
|
61
|
+
puts "Got unexpected error: #{e.class}: #{e.message}"
|
62
|
+
end
|
63
|
+
|
64
|
+
puts "\n=== Same-context test (should work) ==="
|
65
|
+
begin
|
66
|
+
model_a.instance_variable_set(:@api_key, cipher_a) # Back to original
|
67
|
+
result = model_a.api_key
|
68
|
+
result.reveal do |plaintext|
|
69
|
+
puts "SUCCESS: Same-context decryption worked: #{plaintext}"
|
70
|
+
end
|
71
|
+
rescue => e
|
72
|
+
puts "ERROR: Same-context failed: #{e.class}: #{e.message}"
|
73
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true' # Mark as test environment
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Testing load path and setter behavior..."
|
9
|
+
|
10
|
+
class TestModel < Familia::Horreum
|
11
|
+
feature :encrypted_fields
|
12
|
+
identifier_field :id
|
13
|
+
field :id
|
14
|
+
encrypted_field :secret
|
15
|
+
end
|
16
|
+
|
17
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
18
|
+
Familia.config.encryption_keys = test_keys
|
19
|
+
Familia.config.current_key_version = :v1
|
20
|
+
|
21
|
+
# Clean database
|
22
|
+
Familia.dbclient.flushdb
|
23
|
+
|
24
|
+
puts "\n=== TRACING THE SETTER DURING LOAD ==="
|
25
|
+
|
26
|
+
# First, let's monkey-patch the setter to see what's being called
|
27
|
+
original_setter = TestModel.instance_method(:secret=)
|
28
|
+
TestModel.define_method(:secret=) do |value|
|
29
|
+
puts "\n--- SETTER CALLED ---"
|
30
|
+
puts "Setting secret to: #{value}"
|
31
|
+
puts "Value class: #{value.class}"
|
32
|
+
puts "Record exists?: #{exists?}"
|
33
|
+
puts "Record identifier: #{identifier}"
|
34
|
+
|
35
|
+
if value.is_a?(String) && !value.nil?
|
36
|
+
puts "Checking if value is encrypted JSON..."
|
37
|
+
field_type = TestModel.field_types[:secret]
|
38
|
+
is_encrypted = field_type.encrypted_json?(value)
|
39
|
+
puts "encrypted_json? result: #{is_encrypted}"
|
40
|
+
|
41
|
+
if is_encrypted
|
42
|
+
puts "Path: Creating ConcealedString WITHOUT re-encryption"
|
43
|
+
else
|
44
|
+
puts "Path: Will ENCRYPT value and create ConcealedString"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
puts "--- END SETTER ---\n"
|
48
|
+
|
49
|
+
# Call original setter
|
50
|
+
original_setter.bind(self).call(value)
|
51
|
+
end
|
52
|
+
|
53
|
+
puts "\nCreating and saving model..."
|
54
|
+
model = TestModel.new(id: 'test1')
|
55
|
+
puts "Setting secret on in-memory model..."
|
56
|
+
model.secret = 'plaintext-secret'
|
57
|
+
|
58
|
+
puts "\nSaving model..."
|
59
|
+
model.save
|
60
|
+
|
61
|
+
puts "\nRaw data in database:"
|
62
|
+
raw_data = Familia.dbclient.hget('testmodel:test1:object', 'secret')
|
63
|
+
puts "Raw encrypted in DB: #{raw_data[0..100]}..." # Truncate for readability
|
64
|
+
|
65
|
+
puts "\n=== LOADING MODEL ==="
|
66
|
+
loaded_model = TestModel.load('test1')
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true'
|
5
|
+
require 'familia'
|
6
|
+
|
7
|
+
puts "Checking method definition..."
|
8
|
+
|
9
|
+
field_type = Familia::EncryptedFieldType.new(:test)
|
10
|
+
puts "Method source location: #{field_type.method(:encrypted_json?).source_location}"
|
11
|
+
|
12
|
+
# Check if the method exists
|
13
|
+
puts "Method exists?: #{field_type.respond_to?(:encrypted_json?)}"
|
14
|
+
|
15
|
+
# Get the method object and examine it
|
16
|
+
method_obj = field_type.method(:encrypted_json?)
|
17
|
+
puts "Method object: #{method_obj}"
|
18
|
+
puts "Method owner: #{method_obj.owner}"
|
19
|
+
|
20
|
+
# Call with debugging
|
21
|
+
puts "\nCalling method..."
|
22
|
+
result = method_obj.call('{"algorithm":"test"}')
|
23
|
+
puts "Result: #{result}"
|
24
|
+
|
25
|
+
# Let's also check the class directly
|
26
|
+
puts "\nChecking class method..."
|
27
|
+
class_result = Familia::EncryptedFieldType.new(:test2).encrypted_json?('{"algorithm":"test"}')
|
28
|
+
puts "Class result: #{class_result}"
|
29
|
+
|
30
|
+
# Try to define the method inline to see if it works
|
31
|
+
puts "\nDefining method inline..."
|
32
|
+
field_type.define_singleton_method(:test_encrypted_json?) do |data|
|
33
|
+
puts "Inline method called with: #{data}"
|
34
|
+
begin
|
35
|
+
parsed = JSON.parse(data)
|
36
|
+
result = parsed.is_a?(Hash) && parsed.key?('algorithm')
|
37
|
+
puts "Inline result: #{result}"
|
38
|
+
return result
|
39
|
+
rescue => e
|
40
|
+
puts "Inline error: #{e}"
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
inline_result = field_type.test_encrypted_json?('{"algorithm":"test"}')
|
46
|
+
puts "Inline method result: #{inline_result}"
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true' # Mark as test environment
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Testing method resolution for encrypted_json?..."
|
9
|
+
|
10
|
+
class TestModel < Familia::Horreum
|
11
|
+
feature :encrypted_fields
|
12
|
+
identifier_field :id
|
13
|
+
field :id
|
14
|
+
encrypted_field :secret
|
15
|
+
end
|
16
|
+
|
17
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
18
|
+
Familia.config.encryption_keys = test_keys
|
19
|
+
Familia.config.current_key_version = :v1
|
20
|
+
|
21
|
+
field_type = TestModel.field_types[:secret]
|
22
|
+
encrypted_json = '{"algorithm":"xchacha20poly1305","nonce":"test","ciphertext":"test","auth_tag":"test","key_version":"v1"}'
|
23
|
+
|
24
|
+
puts "Field type class: #{field_type.class}"
|
25
|
+
puts "Field type method location: #{field_type.method(:encrypted_json?).source_location}"
|
26
|
+
|
27
|
+
# Test the method directly
|
28
|
+
puts "\nDirect method test:"
|
29
|
+
puts "Result: #{field_type.encrypted_json?(encrypted_json)}"
|
30
|
+
|
31
|
+
# Test with a fresh instance
|
32
|
+
puts "\nCreating fresh EncryptedFieldType instance:"
|
33
|
+
fresh_field_type = Familia::EncryptedFieldType.new(:test)
|
34
|
+
puts "Fresh field type method location: #{fresh_field_type.method(:encrypted_json?).source_location}"
|
35
|
+
puts "Fresh instance result: #{fresh_field_type.encrypted_json?(encrypted_json)}"
|
36
|
+
|
37
|
+
# Test with simple JSON
|
38
|
+
simple_json = '{"algorithm":"test"}'
|
39
|
+
puts "\nTesting with simple JSON: #{simple_json}"
|
40
|
+
puts "Field type result: #{field_type.encrypted_json?(simple_json)}"
|
41
|
+
puts "Fresh instance result: #{fresh_field_type.encrypted_json?(simple_json)}"
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
require 'familia'
|
5
|
+
|
6
|
+
# Minimal test
|
7
|
+
field_type = Familia::EncryptedFieldType.new(:test)
|
8
|
+
json = '{"algorithm":"test"}'
|
9
|
+
|
10
|
+
puts "Testing: #{json}"
|
11
|
+
puts "Result: #{field_type.encrypted_json?(json)}"
|
12
|
+
|
13
|
+
# Manual implementation
|
14
|
+
puts "\nManual check:"
|
15
|
+
begin
|
16
|
+
parsed = JSON.parse(json)
|
17
|
+
puts "Parsed: #{parsed}"
|
18
|
+
puts "Is hash?: #{parsed.is_a?(Hash)}"
|
19
|
+
puts "Has algorithm?: #{parsed.key?('algorithm')}"
|
20
|
+
result = parsed.is_a?(Hash) && parsed.key?('algorithm')
|
21
|
+
puts "Manual result: #{result}"
|
22
|
+
rescue => e
|
23
|
+
puts "Error: #{e}"
|
24
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true' # Mark as test environment
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Testing provider-level decryption..."
|
9
|
+
|
10
|
+
# Setup
|
11
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
12
|
+
Familia.config.encryption_keys = test_keys
|
13
|
+
Familia.config.current_key_version = :v1
|
14
|
+
|
15
|
+
# Simulate the conditions during encryption vs decryption
|
16
|
+
class TestModel < Familia::Horreum
|
17
|
+
feature :encrypted_fields
|
18
|
+
identifier_field :id
|
19
|
+
field :id
|
20
|
+
encrypted_field :secret
|
21
|
+
end
|
22
|
+
|
23
|
+
# Clean database
|
24
|
+
Familia.dbclient.flushdb
|
25
|
+
|
26
|
+
puts "\n=== DIRECT ENCRYPTION/DECRYPTION TEST ==="
|
27
|
+
|
28
|
+
# Test direct encryption/decryption with different AAD values
|
29
|
+
plaintext = "test-secret"
|
30
|
+
context = "TestModel:secret:test1"
|
31
|
+
|
32
|
+
# Scenario 1: Encrypt with AAD = nil (before save)
|
33
|
+
puts "Encrypting with AAD = nil..."
|
34
|
+
encrypted_json_1 = Familia::Encryption.encrypt(plaintext, context: context, additional_data: nil)
|
35
|
+
puts "Encrypted: #{encrypted_json_1}"
|
36
|
+
|
37
|
+
# Try to decrypt with AAD = nil
|
38
|
+
puts "Decrypting with AAD = nil..."
|
39
|
+
decrypted_1 = Familia::Encryption.decrypt(encrypted_json_1, context: context, additional_data: nil)
|
40
|
+
puts "Decrypted: #{decrypted_1}"
|
41
|
+
|
42
|
+
# Try to decrypt with AAD = "test1" (what happens after load)
|
43
|
+
puts "Decrypting with AAD = 'test1'..."
|
44
|
+
begin
|
45
|
+
decrypted_2 = Familia::Encryption.decrypt(encrypted_json_1, context: context, additional_data: "test1")
|
46
|
+
puts "Decrypted: #{decrypted_2}"
|
47
|
+
rescue => e
|
48
|
+
puts "Decryption failed: #{e.class}: #{e.message}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Scenario 2: Encrypt with AAD = "test1" (after save)
|
52
|
+
puts "\nEncrypting with AAD = 'test1'..."
|
53
|
+
encrypted_json_2 = Familia::Encryption.encrypt(plaintext, context: context, additional_data: "test1")
|
54
|
+
puts "Encrypted: #{encrypted_json_2}"
|
55
|
+
|
56
|
+
# Try to decrypt with AAD = "test1"
|
57
|
+
puts "Decrypting with AAD = 'test1'..."
|
58
|
+
decrypted_3 = Familia::Encryption.decrypt(encrypted_json_2, context: context, additional_data: "test1")
|
59
|
+
puts "Decrypted: #{decrypted_3}"
|
60
|
+
|
61
|
+
# Try to decrypt with AAD = nil
|
62
|
+
puts "Decrypting with AAD = nil..."
|
63
|
+
begin
|
64
|
+
decrypted_4 = Familia::Encryption.decrypt(encrypted_json_2, context: context, additional_data: nil)
|
65
|
+
puts "Decrypted: #{decrypted_4}"
|
66
|
+
rescue => e
|
67
|
+
puts "Decryption failed: #{e.class}: #{e.message}"
|
68
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true'
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Debugging secure_by_default_behavior test failure..."
|
9
|
+
|
10
|
+
# Setup encryption keys for testing
|
11
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
12
|
+
Familia.config.encryption_keys = test_keys
|
13
|
+
Familia.config.current_key_version = :v1
|
14
|
+
|
15
|
+
class DebugSecureUserAccount < Familia::Horreum
|
16
|
+
feature :encrypted_fields
|
17
|
+
identifier_field :id
|
18
|
+
field :id
|
19
|
+
field :username
|
20
|
+
field :email
|
21
|
+
encrypted_field :password_hash
|
22
|
+
encrypted_field :api_secret
|
23
|
+
encrypted_field :recovery_key
|
24
|
+
end
|
25
|
+
|
26
|
+
# Patch for debugging
|
27
|
+
class Familia::EncryptedFieldType
|
28
|
+
alias_method :original_encrypt_value, :encrypt_value
|
29
|
+
alias_method :original_decrypt_value, :decrypt_value
|
30
|
+
|
31
|
+
def encrypt_value(record, value)
|
32
|
+
aad = build_aad(record)
|
33
|
+
puts "[ENCRYPT] #{record.class}##{@name}: exists=#{record.exists?}, AAD=#{aad}"
|
34
|
+
original_encrypt_value(record, value)
|
35
|
+
end
|
36
|
+
|
37
|
+
def decrypt_value(record, encrypted)
|
38
|
+
aad = build_aad(record)
|
39
|
+
puts "[DECRYPT] #{record.class}##{@name}: exists=#{record.exists?}, AAD=#{aad}"
|
40
|
+
original_decrypt_value(record, encrypted)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Clean database
|
45
|
+
Familia.dbclient.flushdb
|
46
|
+
|
47
|
+
puts "\n=== CREATION AND ENCRYPTION ==="
|
48
|
+
user = DebugSecureUserAccount.new(id: "user123")
|
49
|
+
puts "After new: exists=#{user.exists?}"
|
50
|
+
|
51
|
+
user.username = "john_doe"
|
52
|
+
user.email = "john@example.com"
|
53
|
+
user.password_hash = "bcrypt$2a$12$abcdef..."
|
54
|
+
user.api_secret = "secret-api-key-12345"
|
55
|
+
|
56
|
+
puts "\n=== SAVE TO DATABASE ==="
|
57
|
+
result = user.save
|
58
|
+
puts "Save result: #{result}"
|
59
|
+
puts "After save: exists=#{user.exists?}"
|
60
|
+
|
61
|
+
puts "\n=== FRESH LOAD FROM DATABASE ==="
|
62
|
+
fresh_user = DebugSecureUserAccount.load("user123")
|
63
|
+
puts "Fresh user loaded: exists=#{fresh_user.exists?}"
|
64
|
+
|
65
|
+
puts "\n=== DECRYPTION ATTEMPT ==="
|
66
|
+
begin
|
67
|
+
fresh_user.password_hash.reveal do |plaintext|
|
68
|
+
puts "SUCCESS: Decrypted password_hash: #{plaintext}"
|
69
|
+
end
|
70
|
+
rescue => e
|
71
|
+
puts "ERROR: #{e.class}: #{e.message}"
|
72
|
+
puts "This suggests AAD mismatch between save and load"
|
73
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true'
|
5
|
+
require 'familia'
|
6
|
+
|
7
|
+
puts "Investigating the string class issue..."
|
8
|
+
|
9
|
+
field_type = Familia::EncryptedFieldType.new(:test)
|
10
|
+
json = '{"algorithm":"test"}'
|
11
|
+
|
12
|
+
puts "Testing string object:"
|
13
|
+
puts "json: #{json}"
|
14
|
+
puts "json.class: #{json.class}"
|
15
|
+
puts "json.is_a?(String): #{json.is_a?(String)}"
|
16
|
+
puts "json.kind_of?(String): #{json.kind_of?(String)}"
|
17
|
+
puts "json.class == String: #{json.class == String}"
|
18
|
+
puts "json.class.ancestors: #{json.class.ancestors}"
|
19
|
+
|
20
|
+
# Let's test with various string types
|
21
|
+
frozen_string = '{"algorithm":"test"}'.freeze
|
22
|
+
puts "\nTesting frozen string:"
|
23
|
+
puts "frozen.class: #{frozen_string.class}"
|
24
|
+
puts "frozen.is_a?(String): #{frozen_string.is_a?(String)}"
|
25
|
+
|
26
|
+
# Test with different string creation methods
|
27
|
+
string_new = String.new('{"algorithm":"test"}')
|
28
|
+
puts "\nTesting String.new:"
|
29
|
+
puts "string_new.class: #{string_new.class}"
|
30
|
+
puts "string_new.is_a?(String): #{string_new.is_a?(String)}"
|
31
|
+
|
32
|
+
# Let's inspect what the method actually receives
|
33
|
+
puts "\nTesting what the method actually receives..."
|
34
|
+
field_type.define_singleton_method(:debug_encrypted_json?) do |data|
|
35
|
+
puts "Received data: #{data.inspect}"
|
36
|
+
puts "Data class: #{data.class}"
|
37
|
+
puts "Data class ancestors: #{data.class.ancestors}"
|
38
|
+
puts "Is String?: #{data.is_a?(String)}"
|
39
|
+
puts "Kind of String?: #{data.kind_of?(String)}"
|
40
|
+
puts "Equals String class?: #{data.class == String}"
|
41
|
+
puts "Responds to string methods?: #{data.respond_to?(:upcase)}"
|
42
|
+
return "debug_complete"
|
43
|
+
end
|
44
|
+
|
45
|
+
result = field_type.debug_encrypted_json?(json)
|
46
|
+
puts "Debug result: #{result}"
|