familia 2.0.0.pre4 → 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/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +11 -8
- data/Gemfile +5 -1
- data/Gemfile.lock +19 -3
- data/README.md +36 -157
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +347 -0
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
- 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 +106 -0
- data/docs/wiki/Implementation-Guide.md +276 -0
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +183 -0
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +18 -27
- data/lib/familia/connection.rb +6 -5
- data/lib/familia/{datatype → data_type}/commands.rb +2 -5
- data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +12 -14
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +119 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
- data/lib/familia/encryption/registry.rb +50 -0
- data/lib/familia/encryption.rb +178 -0
- data/lib/familia/encryption_request_cache.rb +68 -0
- 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 +221 -0
- data/lib/familia/features/encrypted_fields.rb +28 -0
- data/lib/familia/features/expiration.rb +107 -77
- data/lib/familia/features/quantization.rb +5 -9
- data/lib/familia/features/relatable_objects.rb +2 -4
- data/lib/familia/features/safe_dump.rb +14 -17
- data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
- data/lib/familia/features/transient_fields.rb +47 -0
- data/lib/familia/features.rb +40 -24
- data/lib/familia/field_type.rb +273 -0
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
- data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
- data/lib/familia/horreum/subclass/definition.rb +469 -0
- data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +30 -22
- data/lib/familia/logging.rb +14 -14
- data/lib/familia/settings.rb +39 -3
- data/lib/familia/utils.rb +45 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +3 -2
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -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 -5
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +1 -2
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/core/pools_try.rb +2 -2
- data/try/core/secure_identifier_try.rb +0 -1
- data/try/core/settings_try.rb +0 -1
- data/try/core/utils_try.rb +0 -1
- data/try/{datatypes → data_types}/boolean_try.rb +1 -2
- data/try/data_types/counter_try.rb +93 -0
- data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
- data/try/{datatypes → data_types}/hash_try.rb +1 -2
- data/try/{datatypes → data_types}/list_try.rb +1 -2
- data/try/data_types/lock_try.rb +133 -0
- data/try/{datatypes → data_types}/set_try.rb +1 -2
- data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
- data/try/{datatypes → data_types}/string_try.rb +1 -2
- data/try/debugging/README.md +32 -0
- data/try/debugging/cache_behavior_tracer.rb +91 -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/debugging/encryption_method_tracer.rb +138 -0
- data/try/debugging/provider_diagnostics.rb +110 -0
- data/try/edge_cases/hash_symbolization_try.rb +0 -1
- data/try/edge_cases/json_serialization_try.rb +0 -1
- data/try/edge_cases/reserved_keywords_try.rb +42 -11
- data/try/encryption/config_persistence_try.rb +192 -0
- data/try/encryption/encryption_core_try.rb +328 -0
- data/try/encryption/instance_variable_scope_try.rb +31 -0
- data/try/encryption/module_loading_try.rb +28 -0
- data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
- data/try/encryption/roundtrip_validation_try.rb +28 -0
- data/try/encryption/secure_memory_handling_try.rb +125 -0
- data/try/features/encrypted_fields_core_try.rb +125 -0
- data/try/features/encrypted_fields_integration_try.rb +216 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
- data/try/features/encrypted_fields_security_try.rb +377 -0
- data/try/features/encryption_fields/aad_protection_try.rb +138 -0
- data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +141 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
- data/try/features/encryption_fields/fresh_key_try.rb +168 -0
- data/try/features/encryption_fields/key_rotation_try.rb +123 -0
- data/try/features/encryption_fields/memory_security_try.rb +37 -0
- data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/expiration_try.rb +0 -1
- data/try/features/feature_dependencies_try.rb +159 -0
- data/try/features/quantization_try.rb +0 -1
- data/try/features/real_feature_integration_try.rb +148 -0
- data/try/features/relatable_objects_try.rb +0 -1
- data/try/features/safe_dump_advanced_try.rb +0 -1
- data/try/features/safe_dump_try.rb +0 -1
- data/try/features/transient_fields/redacted_string_try.rb +248 -0
- data/try/features/transient_fields/refresh_reset_try.rb +164 -0
- data/try/features/transient_fields/simple_refresh_test.rb +50 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
- data/try/features/transient_fields_core_try.rb +181 -0
- data/try/features/transient_fields_integration_try.rb +260 -0
- data/try/helpers/test_helpers.rb +67 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
- data/try/horreum/field_categories_try.rb +118 -0
- data/try/horreum/field_definition_try.rb +96 -0
- data/try/horreum/initialization_try.rb +1 -2
- data/try/horreum/relations_try.rb +1 -2
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +41 -7
- data/try/memory/memory_basic_test.rb +73 -0
- data/try/memory/memory_detailed_test.rb +121 -0
- data/try/memory/memory_docker_ruby_dump.sh +80 -0
- data/try/memory/memory_search_for_string.rb +83 -0
- data/try/memory/test_actual_redactedstring_protection.rb +38 -0
- data/try/models/customer_safe_dump_try.rb +1 -2
- data/try/models/customer_try.rb +1 -2
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +131 -23
- data/lib/familia/horreum/serialization.rb +0 -445
@@ -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
|
@@ -0,0 +1,138 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Debug script to trace encryption method calls and data flow
|
3
|
+
|
4
|
+
require 'base64'
|
5
|
+
require 'bundler/setup'
|
6
|
+
require_relative '../helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "=== Encryption Method Call Tracer ==="
|
9
|
+
puts
|
10
|
+
|
11
|
+
# Monkey patch to add detailed tracing
|
12
|
+
module Familia
|
13
|
+
module Encryption
|
14
|
+
class << self
|
15
|
+
# Store original methods
|
16
|
+
alias_method :orig_encrypt, :encrypt
|
17
|
+
alias_method :orig_decrypt, :decrypt
|
18
|
+
|
19
|
+
def encrypt(plaintext, context:, additional_data: nil)
|
20
|
+
puts "📤 ENCRYPT called:"
|
21
|
+
puts " Context: #{context}"
|
22
|
+
puts " Plaintext length: #{plaintext&.length} chars"
|
23
|
+
puts " AAD: #{additional_data ? 'present' : 'none'}"
|
24
|
+
|
25
|
+
start_time = Time.now
|
26
|
+
result = orig_encrypt(plaintext, context: context, additional_data: additional_data)
|
27
|
+
elapsed = ((Time.now - start_time) * 1000).round(2)
|
28
|
+
|
29
|
+
puts " Result length: #{result ? result.length : 'nil'} chars"
|
30
|
+
puts " Elapsed: #{elapsed}ms"
|
31
|
+
puts
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
def decrypt(encrypted_json, context:, additional_data: nil)
|
36
|
+
puts "📥 DECRYPT called:"
|
37
|
+
puts " Context: #{context}"
|
38
|
+
puts " Encrypted length: #{encrypted_json&.length} chars"
|
39
|
+
puts " AAD: #{additional_data ? 'present' : 'none'}"
|
40
|
+
|
41
|
+
start_time = Time.now
|
42
|
+
begin
|
43
|
+
result = orig_decrypt(encrypted_json, context: context, additional_data: additional_data)
|
44
|
+
elapsed = ((Time.now - start_time) * 1000).round(2)
|
45
|
+
|
46
|
+
puts " Result length: #{result ? result.length : 'nil'} chars"
|
47
|
+
puts " Elapsed: #{elapsed}ms"
|
48
|
+
puts
|
49
|
+
result
|
50
|
+
rescue => e
|
51
|
+
elapsed = ((Time.now - start_time) * 1000).round(2)
|
52
|
+
puts " ERROR: #{e.class}: #{e.message}"
|
53
|
+
puts " Elapsed: #{elapsed}ms"
|
54
|
+
puts
|
55
|
+
raise
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Manager
|
61
|
+
alias_method :orig_derive_key_with_provider, :derive_key_with_provider
|
62
|
+
|
63
|
+
def derive_key_with_provider(provider, context, version: nil)
|
64
|
+
puts "🔑 DERIVE_KEY called:"
|
65
|
+
puts " Provider: #{provider.class.name}"
|
66
|
+
puts " Context: #{context}"
|
67
|
+
puts " Version: #{version || 'current'}"
|
68
|
+
|
69
|
+
cache = Thread.current[:familia_key_cache] ||= {}
|
70
|
+
cache_key = "#{version || current_key_version}:#{context}"
|
71
|
+
puts " Cache key: #{cache_key}"
|
72
|
+
puts " Cache before: #{cache.keys.inspect}"
|
73
|
+
|
74
|
+
start_time = Time.now
|
75
|
+
result = orig_derive_key_with_provider(provider, context, version: version)
|
76
|
+
elapsed = ((Time.now - start_time) * 1000).round(2)
|
77
|
+
|
78
|
+
cache_after = Thread.current[:familia_key_cache] || {}
|
79
|
+
puts " Cache after: #{cache_after.keys.inspect}"
|
80
|
+
puts " Derived key: [#{result.bytesize} bytes]"
|
81
|
+
puts " Elapsed: #{elapsed}ms"
|
82
|
+
puts
|
83
|
+
result
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Setup test configuration
|
90
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
91
|
+
Familia.config.encryption_keys = test_keys
|
92
|
+
Familia.config.current_key_version = :v1
|
93
|
+
|
94
|
+
# Clear cache
|
95
|
+
Thread.current[:familia_key_cache] = nil
|
96
|
+
|
97
|
+
# Define test model
|
98
|
+
class TraceTestModel < Familia::Horreum
|
99
|
+
feature :encrypted_fields
|
100
|
+
identifier_field :user_id
|
101
|
+
|
102
|
+
field :user_id
|
103
|
+
encrypted_field :password
|
104
|
+
encrypted_field :api_key, aad_fields: [:user_id]
|
105
|
+
end
|
106
|
+
|
107
|
+
puts "=== Test Scenario: Field Operations ==="
|
108
|
+
puts
|
109
|
+
|
110
|
+
puts "Creating model and setting encrypted fields..."
|
111
|
+
user = TraceTestModel.new(user_id: 'trace-user-1')
|
112
|
+
|
113
|
+
puts "Setting password (no AAD)..."
|
114
|
+
user.password = 'secret-password-123'
|
115
|
+
|
116
|
+
puts "Setting api_key (with AAD)..."
|
117
|
+
user.api_key = 'api-key-xyz-789'
|
118
|
+
|
119
|
+
puts "Reading password..."
|
120
|
+
retrieved_password = user.password
|
121
|
+
puts "Final password value: #{retrieved_password}"
|
122
|
+
|
123
|
+
puts "Reading api_key..."
|
124
|
+
retrieved_api_key = user.api_key
|
125
|
+
puts "Final api_key value: #{retrieved_api_key}"
|
126
|
+
|
127
|
+
puts
|
128
|
+
puts "=== Test Scenario: Cross-Algorithm Decryption ==="
|
129
|
+
puts
|
130
|
+
|
131
|
+
# Test cross-algorithm compatibility
|
132
|
+
aes_encrypted = Familia::Encryption.encrypt_with('aes-256-gcm', 'cross-test-data', context: 'cross-test')
|
133
|
+
puts "Decrypting AES-GCM data with default manager..."
|
134
|
+
cross_decrypted = Familia::Encryption.decrypt(aes_encrypted, context: 'cross-test')
|
135
|
+
puts "Cross-algorithm result: #{cross_decrypted}"
|
136
|
+
|
137
|
+
puts
|
138
|
+
puts "=== Method Tracing Complete ==="
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Debug script for testing encryption providers and diagnosing issues
|
3
|
+
|
4
|
+
require 'base64'
|
5
|
+
require_relative '../helpers/test_helpers'
|
6
|
+
|
7
|
+
puts "=== Familia Encryption Provider Diagnostics ==="
|
8
|
+
puts
|
9
|
+
|
10
|
+
# Check encryption system status
|
11
|
+
puts "1. Encryption System Status:"
|
12
|
+
puts " Status: #{Familia::Encryption.status.inspect}"
|
13
|
+
puts
|
14
|
+
|
15
|
+
# Check registry setup
|
16
|
+
puts "2. Registry Providers:"
|
17
|
+
require_relative '../../lib/familia/encryption/registry'
|
18
|
+
Familia::Encryption::Registry.setup!
|
19
|
+
Familia::Encryption::Registry.providers.each do |algo, provider_class|
|
20
|
+
puts " #{algo}: #{provider_class.name}"
|
21
|
+
puts " Available: #{provider_class.available?}"
|
22
|
+
puts " Priority: #{provider_class.priority}"
|
23
|
+
end
|
24
|
+
puts
|
25
|
+
|
26
|
+
# Setup test keys
|
27
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
28
|
+
Familia.config.encryption_keys = test_keys
|
29
|
+
Familia.config.current_key_version = :v1
|
30
|
+
|
31
|
+
# Test each provider individually
|
32
|
+
['xchacha20poly1305', 'aes-256-gcm'].each do |algorithm|
|
33
|
+
puts "3. Testing #{algorithm.upcase} Provider:"
|
34
|
+
|
35
|
+
begin
|
36
|
+
manager = Familia::Encryption::Manager.new(algorithm: algorithm)
|
37
|
+
provider = manager.provider
|
38
|
+
|
39
|
+
puts " Provider class: #{provider.class.name}"
|
40
|
+
puts " Algorithm: #{provider.algorithm}"
|
41
|
+
puts " Nonce size: #{provider.nonce_size} bytes"
|
42
|
+
puts " Auth tag size: #{provider.auth_tag_size} bytes"
|
43
|
+
|
44
|
+
# Test encryption/decryption
|
45
|
+
test_data = "diagnostic test data for #{algorithm}"
|
46
|
+
encrypted = manager.encrypt(test_data, context: 'diagnostics')
|
47
|
+
puts " Encryption: SUCCESS (#{encrypted.length} chars)"
|
48
|
+
|
49
|
+
decrypted = manager.decrypt(encrypted, context: 'diagnostics')
|
50
|
+
success = decrypted == test_data
|
51
|
+
puts " Decryption: #{success ? 'SUCCESS' : 'FAILED'}"
|
52
|
+
|
53
|
+
if !success
|
54
|
+
puts " Expected: #{test_data.inspect}"
|
55
|
+
puts " Got: #{decrypted.inspect}"
|
56
|
+
end
|
57
|
+
|
58
|
+
rescue => e
|
59
|
+
puts " ERROR: #{e.class}: #{e.message}"
|
60
|
+
puts " Backtrace: #{e.backtrace.first(3).join(', ')}"
|
61
|
+
end
|
62
|
+
|
63
|
+
puts
|
64
|
+
end
|
65
|
+
|
66
|
+
# Test cross-algorithm compatibility
|
67
|
+
puts "4. Cross-Algorithm Compatibility Test:"
|
68
|
+
begin
|
69
|
+
xchacha_manager = Familia::Encryption::Manager.new(algorithm: 'xchacha20poly1305')
|
70
|
+
aes_manager = Familia::Encryption::Manager.new(algorithm: 'aes-256-gcm')
|
71
|
+
default_manager = Familia::Encryption::Manager.new
|
72
|
+
|
73
|
+
test_data = "cross-algorithm test"
|
74
|
+
|
75
|
+
# Encrypt with XChaCha20Poly1305
|
76
|
+
xchacha_encrypted = xchacha_manager.encrypt(test_data, context: 'cross-test')
|
77
|
+
xchacha_decrypted = default_manager.decrypt(xchacha_encrypted, context: 'cross-test')
|
78
|
+
puts " XChaCha20Poly1305 -> Default: #{xchacha_decrypted == test_data ? 'SUCCESS' : 'FAILED'}"
|
79
|
+
|
80
|
+
# Encrypt with AES-GCM
|
81
|
+
aes_encrypted = aes_manager.encrypt(test_data, context: 'cross-test')
|
82
|
+
aes_decrypted = default_manager.decrypt(aes_encrypted, context: 'cross-test')
|
83
|
+
puts " AES-GCM -> Default: #{aes_decrypted == test_data ? 'SUCCESS' : 'FAILED'}"
|
84
|
+
|
85
|
+
rescue => e
|
86
|
+
puts " ERROR: #{e.class}: #{e.message}"
|
87
|
+
end
|
88
|
+
puts
|
89
|
+
|
90
|
+
# Test high-level API
|
91
|
+
puts "5. High-Level API Test:"
|
92
|
+
begin
|
93
|
+
encrypted_high = Familia::Encryption.encrypt_with('aes-256-gcm', 'high-level test', context: 'api-test')
|
94
|
+
puts " encrypt_with: SUCCESS"
|
95
|
+
|
96
|
+
# Parse encrypted data to verify structure
|
97
|
+
require 'json'
|
98
|
+
parsed = JSON.parse(encrypted_high, symbolize_names: true)
|
99
|
+
puts " Algorithm stored: #{parsed[:algorithm]}"
|
100
|
+
puts " Key version: #{parsed[:key_version]}"
|
101
|
+
|
102
|
+
decrypted_high = Familia::Encryption.decrypt(encrypted_high, context: 'api-test')
|
103
|
+
puts " decrypt: #{decrypted_high == 'high-level test' ? 'SUCCESS' : 'FAILED'}"
|
104
|
+
|
105
|
+
rescue => e
|
106
|
+
puts " ERROR: #{e.class}: #{e.message}"
|
107
|
+
end
|
108
|
+
|
109
|
+
puts
|
110
|
+
puts "=== Diagnostics Complete ==="
|
@@ -17,7 +17,7 @@ result
|
|
17
17
|
#=!> StandardError
|
18
18
|
|
19
19
|
## prefixed field names work as expected
|
20
|
-
|
20
|
+
ExampleTestClass = Class.new(Familia::Horreum) do
|
21
21
|
identifier_field :email
|
22
22
|
field :email
|
23
23
|
field :secret_ttl
|
@@ -25,7 +25,7 @@ TestClass2 = Class.new(Familia::Horreum) do
|
|
25
25
|
field :dbclient_config
|
26
26
|
end
|
27
27
|
|
28
|
-
user =
|
28
|
+
user = ExampleTestClass.new(email: 'test@example.com')
|
29
29
|
user.secret_ttl = 3600
|
30
30
|
user.user_db = 5
|
31
31
|
user.dbclient_config = { host: 'localhost' }
|
@@ -39,7 +39,7 @@ user.delete!
|
|
39
39
|
result
|
40
40
|
#=> true
|
41
41
|
|
42
|
-
##
|
42
|
+
## Reserved methods still work normally
|
43
43
|
TestClass3 = Class.new(Familia::Horreum) do
|
44
44
|
# Note: Does not enable expiration feature
|
45
45
|
identifier_field :email
|
@@ -55,20 +55,51 @@ user
|
|
55
55
|
#==> _.respond_to?(:logical_database)
|
56
56
|
#==> _.respond_to?(:dbclient)
|
57
57
|
|
58
|
+
## Attempting to pass default_expiration as a field value when instantiating,
|
59
|
+
## when expiration feature is enabled. It doesn't actually change the default
|
60
|
+
## expiration for the instance b/c "default_expiration" is not a regular field.
|
61
|
+
TestClassWithExpirationEnabled1 = Class.new(Familia::Horreum) do
|
62
|
+
feature :expiration
|
63
|
+
identifier_field :email
|
64
|
+
field :email
|
65
|
+
end
|
66
|
+
|
67
|
+
user = TestClassWithExpirationEnabled1.new(email: 'test@example.com', default_expiration: 3600)
|
68
|
+
user.default_expiration
|
69
|
+
#=> 0
|
58
70
|
|
59
|
-
## Attempting to set default_expiration
|
60
|
-
|
71
|
+
## Attempting to set default_expiration for an instance when
|
72
|
+
## the feature is enabled should work
|
73
|
+
TestClassWithExpirationEnabled2 = Class.new(Familia::Horreum) do
|
61
74
|
feature :expiration
|
62
75
|
identifier_field :email
|
63
76
|
field :email
|
64
|
-
field :default_expiration
|
65
77
|
end
|
66
78
|
|
67
|
-
user =
|
68
|
-
user.
|
69
|
-
user.
|
70
|
-
|
71
|
-
|
79
|
+
user = TestClassWithExpirationEnabled2.new(email: 'test@example.com')
|
80
|
+
user.default_expiration = 3601
|
81
|
+
user.default_expiration
|
82
|
+
#=> 3601
|
83
|
+
|
84
|
+
## Attempting to pass default_expiration as a field value when instantiating,
|
85
|
+
## when expiration feature is disabled and then trying to access that value
|
86
|
+
## simply raises a NoMethodError error.
|
87
|
+
TestClassWithExpirationDisabled = Class.new(Familia::Horreum) do
|
88
|
+
identifier_field :email
|
89
|
+
field :email
|
90
|
+
end
|
91
|
+
|
92
|
+
user = TestClassWithExpirationDisabled.new(email: 'test@example.com', default_expiration: 3600)
|
93
|
+
user.default_expiration
|
94
|
+
#=!> NoMethodError
|
95
|
+
|
96
|
+
## Attempting to add a field with a reserved name should raise an error
|
97
|
+
TestClassWithExpirationDisabled = Class.new(Familia::Horreum) do
|
98
|
+
identifier_field :email
|
99
|
+
field :email
|
100
|
+
field :default_expiration
|
101
|
+
end
|
102
|
+
##=!> NoMethodError
|
72
103
|
|
73
104
|
## prefixed field names work as expected
|
74
105
|
TestClass5 = Class.new(Familia::Horreum) do
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# try/encryption/debug2_try.rb
|
2
|
+
|
3
|
+
# - Tests configuration persistence between test sections
|
4
|
+
# - Validates that config can be set and accessed in tryouts
|
5
|
+
|
6
|
+
require 'base64'
|
7
|
+
|
8
|
+
require_relative '../helpers/test_helpers'
|
9
|
+
require 'familia/encryption/providers/xchacha20_poly1305_provider'
|
10
|
+
|
11
|
+
# SETUP
|
12
|
+
Familia.config.encryption_keys = {
|
13
|
+
v1: Base64.strict_encode64('a' * 32)
|
14
|
+
}
|
15
|
+
Familia.config.current_key_version = :v1
|
16
|
+
|
17
|
+
## Check config in test
|
18
|
+
keys = Familia.config.encryption_keys
|
19
|
+
version = Familia.config.current_key_version
|
20
|
+
[keys.nil?, version.nil?]
|
21
|
+
#=> [false, false]
|
22
|
+
|
23
|
+
## Try basic encryption in test
|
24
|
+
Familia.config.encryption_keys = {v1: Base64.strict_encode64('a' * 32)}
|
25
|
+
Familia.config.current_key_version = :v1
|
26
|
+
result = Familia::Encryption.encrypt('test', context: 'test')
|
27
|
+
result.nil?
|
28
|
+
#=> false
|
29
|
+
|
30
|
+
## XChaCha20Poly1305Provider is available when RbNaCl is loaded
|
31
|
+
@provider_class = Familia::Encryption::Providers::XChaCha20Poly1305Provider
|
32
|
+
@provider_class.available?
|
33
|
+
#=> true
|
34
|
+
|
35
|
+
## Provider has highest priority
|
36
|
+
@provider_class.priority
|
37
|
+
#=> 100
|
38
|
+
|
39
|
+
## derive_key generates 32-byte key from master key and context
|
40
|
+
provider = @provider_class.new
|
41
|
+
master_key = 'a' * 32
|
42
|
+
context = 'TestModel:field:user123'
|
43
|
+
derived_key = provider.derive_key(master_key, context)
|
44
|
+
derived_key.bytesize
|
45
|
+
#=> 32
|
46
|
+
|
47
|
+
## derive_key with same inputs produces same output
|
48
|
+
provider = @provider_class.new
|
49
|
+
master_key = 'a' * 32
|
50
|
+
context = 'TestModel:field:user123'
|
51
|
+
key1 = provider.derive_key(master_key, context)
|
52
|
+
key2 = provider.derive_key(master_key, context)
|
53
|
+
key1 == key2
|
54
|
+
#=> true
|
55
|
+
|
56
|
+
## derive_key with different contexts produces different keys
|
57
|
+
provider = @provider_class.new
|
58
|
+
master_key = 'a' * 32
|
59
|
+
context1 = 'TestModel:field:user123'
|
60
|
+
context2 = 'TestModel:field:user456'
|
61
|
+
key1 = provider.derive_key(master_key, context1)
|
62
|
+
key2 =
|
63
|
+
provider.derive_key(master_key, context2)
|
64
|
+
key1 != key2
|
65
|
+
#=> true
|
66
|
+
|
67
|
+
## derive_key with custom personalization works
|
68
|
+
provider = @provider_class.new
|
69
|
+
master_key = 'a' * 32
|
70
|
+
context = 'TestModel:field:user123'
|
71
|
+
personal = 'custom_app_v2'
|
72
|
+
derived_key = provider.derive_key(master_key, context, personal: personal)
|
73
|
+
derived_key.bytesize
|
74
|
+
#=> 32
|
75
|
+
|
76
|
+
## derive_key rejects personalization string with null bytes
|
77
|
+
provider = @provider_class.new
|
78
|
+
master_key = 'a' * 32
|
79
|
+
context = 'TestModel:field:user123'
|
80
|
+
personal_with_null = "app\0version"
|
81
|
+
begin
|
82
|
+
provider.derive_key(master_key, context, personal: personal_with_null)
|
83
|
+
"should_not_reach_here"
|
84
|
+
rescue Familia::EncryptionError => e
|
85
|
+
e.message
|
86
|
+
end
|
87
|
+
#=> "Personalization string must not contain null bytes"
|
88
|
+
|
89
|
+
## derive_key rejects config personalization with null bytes
|
90
|
+
provider = @provider_class.new
|
91
|
+
master_key = 'a' * 32
|
92
|
+
context = 'TestModel:field:user123'
|
93
|
+
# Set config with null byte
|
94
|
+
original_personal = Familia.config.encryption_personalization
|
95
|
+
Familia.config.encryption_personalization = "bad\0config"
|
96
|
+
begin
|
97
|
+
provider.derive_key(master_key, context)
|
98
|
+
"should_not_reach_here"
|
99
|
+
rescue Familia::EncryptionError => e
|
100
|
+
e.message
|
101
|
+
ensure
|
102
|
+
Familia.config.encryption_personalization = original_personal
|
103
|
+
end
|
104
|
+
#=> "Personalization string must not contain null bytes"
|
105
|
+
|
106
|
+
## derive_key validates master key length
|
107
|
+
provider = @provider_class.new
|
108
|
+
short_key = 'a' * 16 # Too short
|
109
|
+
context = 'TestModel:field:user123'
|
110
|
+
begin
|
111
|
+
provider.derive_key(short_key, context)
|
112
|
+
"should_not_reach_here"
|
113
|
+
rescue Familia::EncryptionError => e
|
114
|
+
e.message
|
115
|
+
end
|
116
|
+
#=> "Key must be at least 32 bytes"
|
117
|
+
|
118
|
+
## derive_key rejects nil master key
|
119
|
+
provider = @provider_class.new
|
120
|
+
context = 'TestModel:field:user123'
|
121
|
+
begin
|
122
|
+
provider.derive_key(nil, context)
|
123
|
+
"should_not_reach_here"
|
124
|
+
rescue Familia::EncryptionError => e
|
125
|
+
e.message
|
126
|
+
end
|
127
|
+
#=> "Key cannot be nil"
|
128
|
+
|
129
|
+
## encrypt/decrypt round trip with derived key works
|
130
|
+
provider = @provider_class.new
|
131
|
+
master_key = 'a' * 32
|
132
|
+
context = 'TestModel:field:user123'
|
133
|
+
derived_key = provider.derive_key(master_key, context)
|
134
|
+
plaintext = 'sensitive data'
|
135
|
+
encrypted_data = provider.encrypt(plaintext, derived_key)
|
136
|
+
decrypted = provider.decrypt(
|
137
|
+
encrypted_data[:ciphertext],
|
138
|
+
derived_key,
|
139
|
+
encrypted_data[:nonce],
|
140
|
+
encrypted_data[:auth_tag]
|
141
|
+
)
|
142
|
+
decrypted
|
143
|
+
#=> "sensitive data"
|
144
|
+
|
145
|
+
## encrypt with additional data and derived key
|
146
|
+
provider = @provider_class.new
|
147
|
+
master_key = 'a' * 32
|
148
|
+
context = 'TestModel:field:user123'
|
149
|
+
derived_key = provider.derive_key(master_key, context)
|
150
|
+
plaintext = 'sensitive data'
|
151
|
+
additional_data = 'user_id:123'
|
152
|
+
encrypted_data = provider.encrypt(plaintext, derived_key, additional_data)
|
153
|
+
decrypted = provider.decrypt(
|
154
|
+
encrypted_data[:ciphertext],
|
155
|
+
derived_key,
|
156
|
+
encrypted_data[:nonce],
|
157
|
+
encrypted_data[:auth_tag],
|
158
|
+
additional_data
|
159
|
+
)
|
160
|
+
decrypted
|
161
|
+
#=> "sensitive data"
|
162
|
+
|
163
|
+
## generate_nonce produces correct size
|
164
|
+
provider = @provider_class.new
|
165
|
+
nonce = provider.generate_nonce
|
166
|
+
nonce.bytesize
|
167
|
+
#=> 24
|
168
|
+
|
169
|
+
## generate_nonce produces unique values
|
170
|
+
provider = @provider_class.new
|
171
|
+
nonce1 = provider.generate_nonce
|
172
|
+
nonce2 = provider.generate_nonce
|
173
|
+
nonce1 != nonce2
|
174
|
+
#=> true
|
175
|
+
|
176
|
+
## secure_wipe works with valid key
|
177
|
+
provider = @provider_class.new
|
178
|
+
key = 'a' * 32
|
179
|
+
provider.secure_wipe(key)
|
180
|
+
# Should not raise error
|
181
|
+
true
|
182
|
+
#=> true
|
183
|
+
|
184
|
+
## secure_wipe handles nil key gracefully
|
185
|
+
provider = @provider_class.new
|
186
|
+
provider.secure_wipe(nil)
|
187
|
+
# Should not raise error
|
188
|
+
true
|
189
|
+
#=> true
|
190
|
+
|
191
|
+
# TEARDOWN
|
192
|
+
Thread.current[:familia_key_cache]&.clear if Thread.current[:familia_key_cache]
|