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,93 @@
|
|
1
|
+
# try/data_types/counter_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
@a = Bone.new(token: 'atoken3')
|
6
|
+
|
7
|
+
## Bone#dbkey
|
8
|
+
@a.dbkey
|
9
|
+
#=> 'bone:atoken3:object'
|
10
|
+
|
11
|
+
## Familia::Counter should have default value of 0
|
12
|
+
@a.counter.value
|
13
|
+
#=> 0
|
14
|
+
|
15
|
+
## Familia::Counter#value=
|
16
|
+
@a.counter.value = 42
|
17
|
+
#=> 42
|
18
|
+
|
19
|
+
## Familia::Counter#to_i
|
20
|
+
@a.counter.to_i
|
21
|
+
#=> 42
|
22
|
+
|
23
|
+
## Familia::Counter#to_s
|
24
|
+
@a.counter.to_s
|
25
|
+
#=> '42'
|
26
|
+
|
27
|
+
## Familia::Counter#increment
|
28
|
+
@a.counter.increment
|
29
|
+
#=> 43
|
30
|
+
|
31
|
+
## Familia::Counter#incrementby
|
32
|
+
@a.counter.incrementby(10)
|
33
|
+
#=> 53
|
34
|
+
|
35
|
+
## Familia::Counter#decrement
|
36
|
+
@a.counter.decrement
|
37
|
+
#=> 52
|
38
|
+
|
39
|
+
## Familia::Counter#decrementby
|
40
|
+
@a.counter.decrementby(5)
|
41
|
+
#=> 47
|
42
|
+
|
43
|
+
## Familia::Counter#reset with value
|
44
|
+
@a.counter.reset(100)
|
45
|
+
#=> true
|
46
|
+
|
47
|
+
## Familia::Counter#reset without value (defaults to 0)
|
48
|
+
@a.counter.reset
|
49
|
+
@a.counter.reset
|
50
|
+
@a.counter.value
|
51
|
+
#=> 0
|
52
|
+
|
53
|
+
## Familia::Counter#atomic_increment_and_get
|
54
|
+
@a.counter.atomic_increment_and_get(25)
|
55
|
+
#=> 25
|
56
|
+
|
57
|
+
## Familia::Counter#increment_if_less_than (success case)
|
58
|
+
@a.counter.increment_if_less_than(50, 10)
|
59
|
+
#=> true
|
60
|
+
|
61
|
+
## Familia::Counter#value after conditional increment
|
62
|
+
@a.counter.to_i
|
63
|
+
#=> 35
|
64
|
+
|
65
|
+
## Familia::Counter#increment_if_less_than (failure case)
|
66
|
+
@a.counter.increment_if_less_than(30, 10)
|
67
|
+
#=> false
|
68
|
+
|
69
|
+
## Familia::Counter#value unchanged after failed conditional increment
|
70
|
+
@a.counter.to_i
|
71
|
+
#=> 35
|
72
|
+
|
73
|
+
## Familia::Counter.new standalone
|
74
|
+
@counter = Familia::Counter.new 'test:counter'
|
75
|
+
@counter.dbkey
|
76
|
+
#=> 'test:counter'
|
77
|
+
|
78
|
+
## Standalone counter starts at 0
|
79
|
+
@counter.value
|
80
|
+
#=> 0
|
81
|
+
|
82
|
+
## Standalone counter increment
|
83
|
+
@counter.increment
|
84
|
+
#=> 1
|
85
|
+
|
86
|
+
## Standalone counter set string value gets coerced to integer
|
87
|
+
@counter.value = "123"
|
88
|
+
@counter.to_i
|
89
|
+
#=> 123
|
90
|
+
|
91
|
+
# Cleanup
|
92
|
+
@a.counter.delete!
|
93
|
+
@counter.delete!
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# try/data_types/lock_try.rb
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
@a = Bone.new(token: 'atoken4')
|
6
|
+
|
7
|
+
## Bone#dbkey
|
8
|
+
@a.dbkey
|
9
|
+
#=> 'bone:atoken4:object'
|
10
|
+
|
11
|
+
## Familia::Lock should start unlocked
|
12
|
+
@a.lock.locked?
|
13
|
+
#=> false
|
14
|
+
|
15
|
+
## Familia::Lock#value should be nil when unlocked
|
16
|
+
@a.lock.value
|
17
|
+
#=> nil
|
18
|
+
|
19
|
+
## Familia::Lock#acquire returns token when successful
|
20
|
+
@token1 = @a.lock.acquire
|
21
|
+
@token1.class
|
22
|
+
#=> String
|
23
|
+
|
24
|
+
## Familia::Lock#locked? after acquire
|
25
|
+
@a.lock.locked?
|
26
|
+
#=> true
|
27
|
+
|
28
|
+
## Familia::Lock#held_by? with correct token
|
29
|
+
@a.lock.held_by?(@token1)
|
30
|
+
#=> true
|
31
|
+
|
32
|
+
## Familia::Lock#held_by? with wrong token
|
33
|
+
@a.lock.held_by?('wrong-token')
|
34
|
+
#=> false
|
35
|
+
|
36
|
+
## Familia::Lock#acquire when already locked returns false
|
37
|
+
@a.lock.acquire
|
38
|
+
#=> false
|
39
|
+
|
40
|
+
## Familia::Lock#release with correct token
|
41
|
+
@a.lock.release(@token1)
|
42
|
+
#=> true
|
43
|
+
|
44
|
+
## Familia::Lock#locked? after release
|
45
|
+
@a.lock.locked?
|
46
|
+
#=> false
|
47
|
+
|
48
|
+
## Familia::Lock#release with wrong token (lock not held)
|
49
|
+
@a.lock.release('wrong-token')
|
50
|
+
#=> false
|
51
|
+
|
52
|
+
## Familia::Lock#acquire with custom token
|
53
|
+
@custom_token = 'my-custom-token-123'
|
54
|
+
@result = @a.lock.acquire(@custom_token)
|
55
|
+
@result
|
56
|
+
#=> 'my-custom-token-123'
|
57
|
+
|
58
|
+
## Familia::Lock#held_by? with custom token
|
59
|
+
@a.lock.held_by?(@custom_token)
|
60
|
+
#=> true
|
61
|
+
|
62
|
+
## Familia::Lock#force_unlock!
|
63
|
+
@a.lock.force_unlock!
|
64
|
+
#=> true
|
65
|
+
|
66
|
+
## Familia::Lock#locked? after force unlock
|
67
|
+
@a.lock.locked?
|
68
|
+
#=> false
|
69
|
+
|
70
|
+
## Familia::Lock.new standalone
|
71
|
+
@lock = Familia::Lock.new 'test:lock'
|
72
|
+
@lock.dbkey
|
73
|
+
#=> 'test:lock'
|
74
|
+
|
75
|
+
## Standalone lock starts unlocked
|
76
|
+
@lock.locked?
|
77
|
+
#=> false
|
78
|
+
|
79
|
+
## Standalone lock acquire
|
80
|
+
@standalone_token = @lock.acquire
|
81
|
+
@standalone_token.class
|
82
|
+
#=> String
|
83
|
+
|
84
|
+
## Standalone lock is now locked
|
85
|
+
@lock.locked?
|
86
|
+
#=> true
|
87
|
+
|
88
|
+
## Standalone lock acquire with TTL
|
89
|
+
@lock.force_unlock!
|
90
|
+
@ttl_token = @lock.acquire('ttl-token', ttl: 1)
|
91
|
+
@ttl_token
|
92
|
+
#=> 'ttl-token'
|
93
|
+
|
94
|
+
## Wait for TTL expiration and check if lock auto-expires
|
95
|
+
# Note: This test might be flaky in fast test runs
|
96
|
+
sleep 2
|
97
|
+
@lock.locked?
|
98
|
+
#=> false
|
99
|
+
|
100
|
+
## Acquire with zero TTL should return false
|
101
|
+
@lock2 = Familia::Lock.new 'test:lock2'
|
102
|
+
@lock2.acquire('zero-ttl', ttl: 0)
|
103
|
+
#=> false
|
104
|
+
|
105
|
+
## Lock should not be held after zero TTL rejection
|
106
|
+
@lock2.locked?
|
107
|
+
#=> false
|
108
|
+
|
109
|
+
## Acquire with negative TTL should return false
|
110
|
+
@lock2.acquire('neg-ttl', ttl: -5)
|
111
|
+
#=> false
|
112
|
+
|
113
|
+
## Lock should not be held after negative TTL rejection
|
114
|
+
@lock2.locked?
|
115
|
+
#=> false
|
116
|
+
|
117
|
+
## Acquire with nil TTL should work (no expiration)
|
118
|
+
@nil_ttl_token = @lock2.acquire('no-expiry', ttl: nil)
|
119
|
+
@nil_ttl_token
|
120
|
+
#=> 'no-expiry'
|
121
|
+
|
122
|
+
## Lock with nil TTL should be held
|
123
|
+
@lock2.locked?
|
124
|
+
#=> true
|
125
|
+
|
126
|
+
## Lock with nil TTL should not have expiration
|
127
|
+
@lock2.current_expiration
|
128
|
+
#=> -1
|
129
|
+
|
130
|
+
## Cleanup
|
131
|
+
@a.lock.delete!
|
132
|
+
@lock.delete!
|
133
|
+
@lock2.delete!
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true'
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Debugging AAD during encrypt/decrypt process..."
|
9
|
+
|
10
|
+
# Setup encryption keys
|
11
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
12
|
+
Familia.config.encryption_keys = test_keys
|
13
|
+
Familia.config.current_key_version = :v1
|
14
|
+
|
15
|
+
class DebugModelA < Familia::Horreum
|
16
|
+
feature :encrypted_fields
|
17
|
+
identifier_field :id
|
18
|
+
field :id
|
19
|
+
encrypted_field :api_key
|
20
|
+
end
|
21
|
+
|
22
|
+
class DebugModelB < Familia::Horreum
|
23
|
+
feature :encrypted_fields
|
24
|
+
identifier_field :id
|
25
|
+
field :id
|
26
|
+
encrypted_field :api_key
|
27
|
+
end
|
28
|
+
|
29
|
+
# Patch the EncryptedFieldType to add debug output
|
30
|
+
class Familia::EncryptedFieldType
|
31
|
+
alias_method :original_encrypt_value, :encrypt_value
|
32
|
+
alias_method :original_decrypt_value, :decrypt_value
|
33
|
+
alias_method :original_build_aad, :build_aad
|
34
|
+
|
35
|
+
def encrypt_value(record, value)
|
36
|
+
context = build_context(record)
|
37
|
+
aad = build_aad(record)
|
38
|
+
puts "[ENCRYPT] Class: #{record.class}, ID: #{record.identifier}, Context: #{context}, AAD: #{aad}"
|
39
|
+
original_encrypt_value(record, value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def decrypt_value(record, encrypted)
|
43
|
+
context = build_context(record)
|
44
|
+
aad = build_aad(record)
|
45
|
+
puts "[DECRYPT] Class: #{record.class}, ID: #{record.identifier}, Context: #{context}, AAD: #{aad}"
|
46
|
+
original_decrypt_value(record, encrypted)
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_aad(record)
|
50
|
+
aad = original_build_aad(record)
|
51
|
+
puts "[BUILD_AAD] Class: #{record.class}, ID: #{record.identifier}, AAD: #{aad}"
|
52
|
+
aad
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Clean database
|
57
|
+
Familia.dbclient.flushdb
|
58
|
+
|
59
|
+
model_a = DebugModelA.new(id: 'same-id')
|
60
|
+
model_b = DebugModelB.new(id: 'same-id')
|
61
|
+
|
62
|
+
puts "\n=== ENCRYPTION PHASE ==="
|
63
|
+
puts "Encrypting for ModelA:"
|
64
|
+
model_a.api_key = 'secret-key'
|
65
|
+
cipher_a = model_a.instance_variable_get(:@api_key)
|
66
|
+
|
67
|
+
puts "\nEncrypting for ModelB:"
|
68
|
+
model_b.api_key = 'secret-key'
|
69
|
+
cipher_b = model_b.instance_variable_get(:@api_key)
|
70
|
+
|
71
|
+
puts "\n=== DECRYPTION PHASE - Same Context ==="
|
72
|
+
puts "Decrypting ModelA with ModelA context:"
|
73
|
+
model_a.api_key.reveal { |plain| puts "Result: #{plain}" }
|
74
|
+
|
75
|
+
puts "\n=== DECRYPTION PHASE - Cross Context ==="
|
76
|
+
puts "Setting ModelB cipher into ModelA and trying to decrypt:"
|
77
|
+
model_a.instance_variable_set(:@api_key, cipher_b)
|
78
|
+
begin
|
79
|
+
model_a.api_key.reveal { |plain| puts "ERROR: Cross-context worked: #{plain}" }
|
80
|
+
rescue => e
|
81
|
+
puts "SUCCESS: Cross-context failed as expected: #{e.class}: #{e.message}"
|
82
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true' # Mark as test environment
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Testing ConcealedString internal call chain..."
|
9
|
+
|
10
|
+
class TestModel < Familia::Horreum
|
11
|
+
feature :encrypted_fields
|
12
|
+
identifier_field :id
|
13
|
+
field :id
|
14
|
+
encrypted_field :secret
|
15
|
+
end
|
16
|
+
|
17
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
18
|
+
Familia.config.encryption_keys = test_keys
|
19
|
+
Familia.config.current_key_version = :v1
|
20
|
+
|
21
|
+
# Clean database
|
22
|
+
Familia.dbclient.flushdb
|
23
|
+
|
24
|
+
puts "\n=== CONCEALED STRING INTERNAL DEBUGGING ==="
|
25
|
+
|
26
|
+
# Create and save model
|
27
|
+
model = TestModel.new(id: 'test1')
|
28
|
+
model.secret = 'plaintext-secret'
|
29
|
+
model.save
|
30
|
+
|
31
|
+
# Load model from database
|
32
|
+
loaded_model = TestModel.load('test1')
|
33
|
+
concealed_string = loaded_model.secret
|
34
|
+
|
35
|
+
# Access internal ConcealedString components
|
36
|
+
puts "ConcealedString @encrypted_data: #{concealed_string.instance_variable_get(:@encrypted_data)}"
|
37
|
+
puts "ConcealedString @record: #{concealed_string.instance_variable_get(:@record)}"
|
38
|
+
puts "ConcealedString @field_type: #{concealed_string.instance_variable_get(:@field_type)}"
|
39
|
+
|
40
|
+
record = concealed_string.instance_variable_get(:@record)
|
41
|
+
field_type = concealed_string.instance_variable_get(:@field_type)
|
42
|
+
encrypted_data = concealed_string.instance_variable_get(:@encrypted_data)
|
43
|
+
|
44
|
+
puts "\n=== STEP BY STEP DECRYPTION ==="
|
45
|
+
puts "Record class: #{record.class}"
|
46
|
+
puts "Record identifier: #{record.identifier}"
|
47
|
+
puts "Record exists?: #{record.exists?}"
|
48
|
+
puts "Field type class: #{field_type.class}"
|
49
|
+
|
50
|
+
# Test the exact same call that ConcealedString.reveal makes
|
51
|
+
puts "\nCalling field_type.decrypt_value(record, encrypted_data)..."
|
52
|
+
begin
|
53
|
+
result = field_type.decrypt_value(record, encrypted_data)
|
54
|
+
puts "decrypt_value result: #{result}"
|
55
|
+
puts "Result class: #{result.class}"
|
56
|
+
rescue => e
|
57
|
+
puts "decrypt_value ERROR: #{e.class}: #{e.message}"
|
58
|
+
puts e.backtrace.first(10)
|
59
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true' # Mark as test environment
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Testing ConcealedString.reveal error handling..."
|
9
|
+
|
10
|
+
class TestModel < Familia::Horreum
|
11
|
+
feature :encrypted_fields
|
12
|
+
identifier_field :id
|
13
|
+
field :id
|
14
|
+
encrypted_field :secret
|
15
|
+
end
|
16
|
+
|
17
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
18
|
+
Familia.config.encryption_keys = test_keys
|
19
|
+
Familia.config.current_key_version = :v1
|
20
|
+
|
21
|
+
# Clean database
|
22
|
+
Familia.dbclient.flushdb
|
23
|
+
|
24
|
+
puts "\n=== CONCEALED STRING REVEAL ERROR PATH ==="
|
25
|
+
|
26
|
+
# Create and save model (encrypt with AAD = nil)
|
27
|
+
model = TestModel.new(id: 'test1')
|
28
|
+
model.secret = 'plaintext-secret'
|
29
|
+
model.save
|
30
|
+
|
31
|
+
# Load model from database (decrypt with AAD = "test1")
|
32
|
+
loaded_model = TestModel.load('test1')
|
33
|
+
|
34
|
+
puts "Loaded model secret class: #{loaded_model.secret.class}"
|
35
|
+
|
36
|
+
# Test ConcealedString.reveal directly
|
37
|
+
concealed_string = loaded_model.secret
|
38
|
+
puts "ConcealedString object: #{concealed_string.inspect}"
|
39
|
+
|
40
|
+
puts "\nTesting ConcealedString.reveal..."
|
41
|
+
begin
|
42
|
+
result = concealed_string.reveal do |plaintext|
|
43
|
+
puts "Inside reveal block, plaintext: #{plaintext}"
|
44
|
+
plaintext # Return the plaintext
|
45
|
+
end
|
46
|
+
puts "Reveal result: #{result}"
|
47
|
+
puts "Result class: #{result.class}"
|
48
|
+
rescue => e
|
49
|
+
puts "Reveal ERROR: #{e.class}: #{e.message}"
|
50
|
+
puts e.backtrace.first(5)
|
51
|
+
end
|
52
|
+
|
53
|
+
puts "\nTesting ConcealedString.reveal_for_testing..."
|
54
|
+
begin
|
55
|
+
result = concealed_string.reveal_for_testing
|
56
|
+
puts "reveal_for_testing result: #{result}"
|
57
|
+
puts "Result class: #{result.class}"
|
58
|
+
rescue => e
|
59
|
+
puts "reveal_for_testing ERROR: #{e.class}: #{e.message}"
|
60
|
+
puts e.backtrace.first(5)
|
61
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true'
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Debugging context and AAD generation..."
|
9
|
+
|
10
|
+
# Setup encryption keys
|
11
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
12
|
+
Familia.config.encryption_keys = test_keys
|
13
|
+
Familia.config.current_key_version = :v1
|
14
|
+
|
15
|
+
class ModelA < Familia::Horreum
|
16
|
+
feature :encrypted_fields
|
17
|
+
identifier_field :id
|
18
|
+
field :id
|
19
|
+
encrypted_field :api_key
|
20
|
+
end
|
21
|
+
|
22
|
+
class ModelB < Familia::Horreum
|
23
|
+
feature :encrypted_fields
|
24
|
+
identifier_field :id
|
25
|
+
field :id
|
26
|
+
encrypted_field :api_key
|
27
|
+
end
|
28
|
+
|
29
|
+
model_a = ModelA.new(id: 'same-id')
|
30
|
+
model_b = ModelB.new(id: 'same-id')
|
31
|
+
|
32
|
+
# Get the field type for each model
|
33
|
+
field_type_a = ModelA.fields[:api_key]
|
34
|
+
field_type_b = ModelB.fields[:api_key]
|
35
|
+
|
36
|
+
puts "=== Context and AAD Analysis ==="
|
37
|
+
|
38
|
+
# Test context generation
|
39
|
+
context_a = field_type_a.send(:build_context, model_a)
|
40
|
+
context_b = field_type_b.send(:build_context, model_b)
|
41
|
+
|
42
|
+
puts "ModelA context: #{context_a}"
|
43
|
+
puts "ModelB context: #{context_b}"
|
44
|
+
puts "Contexts match: #{context_a == context_b}"
|
45
|
+
|
46
|
+
# Test AAD generation
|
47
|
+
aad_a = field_type_a.send(:build_aad, model_a)
|
48
|
+
aad_b = field_type_b.send(:build_aad, model_b)
|
49
|
+
|
50
|
+
puts "\nModelA AAD: #{aad_a}"
|
51
|
+
puts "ModelB AAD: #{aad_b}"
|
52
|
+
puts "AADs match: #{aad_a == aad_b}"
|
53
|
+
|
54
|
+
puts "\n=== Testing different identifiers ==="
|
55
|
+
model_a2 = ModelA.new(id: 'different-id')
|
56
|
+
model_b2 = ModelB.new(id: 'different-id')
|
57
|
+
|
58
|
+
context_a2 = field_type_a.send(:build_context, model_a2)
|
59
|
+
context_b2 = field_type_b.send(:build_context, model_b2)
|
60
|
+
aad_a2 = field_type_a.send(:build_aad, model_a2)
|
61
|
+
aad_b2 = field_type_b.send(:build_aad, model_b2)
|
62
|
+
|
63
|
+
puts "ModelA2 context: #{context_a2}"
|
64
|
+
puts "ModelB2 context: #{context_b2}"
|
65
|
+
puts "A2-B2 contexts match: #{context_a2 == context_b2}"
|
66
|
+
puts "ModelA2 AAD: #{aad_a2}"
|
67
|
+
puts "ModelB2 AAD: #{aad_b2}"
|
68
|
+
puts "A2-B2 AADs match: #{aad_a2 == aad_b2}"
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true'
|
5
|
+
require 'familia'
|
6
|
+
|
7
|
+
puts "Understanding the issue with cross-context decryption..."
|
8
|
+
|
9
|
+
# Setup encryption keys
|
10
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
11
|
+
Familia.config.encryption_keys = test_keys
|
12
|
+
Familia.config.current_key_version = :v1
|
13
|
+
|
14
|
+
class ModelA < Familia::Horreum
|
15
|
+
feature :encrypted_fields
|
16
|
+
identifier_field :id
|
17
|
+
field :id
|
18
|
+
encrypted_field :api_key
|
19
|
+
end
|
20
|
+
|
21
|
+
class ModelB < Familia::Horreum
|
22
|
+
feature :encrypted_fields
|
23
|
+
identifier_field :id
|
24
|
+
field :id
|
25
|
+
encrypted_field :api_key
|
26
|
+
end
|
27
|
+
|
28
|
+
model_a = ModelA.new(id: 'same-id')
|
29
|
+
model_b = ModelB.new(id: 'same-id')
|
30
|
+
|
31
|
+
puts "ModelA class: #{model_a.class}"
|
32
|
+
puts "ModelB class: #{model_b.class}"
|
33
|
+
puts "Same class?: #{model_a.class == model_b.class}"
|
34
|
+
|
35
|
+
# Inspect what context would be built
|
36
|
+
puts "\nContext analysis:"
|
37
|
+
puts "ModelA identifier: #{model_a.identifier}"
|
38
|
+
puts "ModelB identifier: #{model_b.identifier}"
|
39
|
+
|
40
|
+
# Let's see what happens with the field types
|
41
|
+
modelA_api_key_type = nil
|
42
|
+
modelB_api_key_type = nil
|
43
|
+
|
44
|
+
ModelA.field_types.each do |name, type|
|
45
|
+
if name == :api_key
|
46
|
+
modelA_api_key_type = type
|
47
|
+
break
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
ModelB.field_types.each do |name, type|
|
52
|
+
if name == :api_key
|
53
|
+
modelB_api_key_type = type
|
54
|
+
break
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
puts "ModelA api_key type: #{modelA_api_key_type}"
|
59
|
+
puts "ModelB api_key type: #{modelB_api_key_type}"
|
60
|
+
puts "Same type object?: #{modelA_api_key_type.object_id == modelB_api_key_type.object_id}"
|
61
|
+
|
62
|
+
# Check what context and AAD would be generated
|
63
|
+
if modelA_api_key_type && modelB_api_key_type
|
64
|
+
context_a = "#{model_a.class.name}:api_key:#{model_a.identifier}"
|
65
|
+
context_b = "#{model_b.class.name}:api_key:#{model_b.identifier}"
|
66
|
+
|
67
|
+
puts "\nExpected contexts:"
|
68
|
+
puts "ModelA context: #{context_a}"
|
69
|
+
puts "ModelB context: #{context_b}"
|
70
|
+
puts "Contexts should be different: #{context_a != context_b}"
|
71
|
+
|
72
|
+
# AAD should be identifier when no aad_fields
|
73
|
+
aad_a = model_a.identifier
|
74
|
+
aad_b = model_b.identifier
|
75
|
+
|
76
|
+
puts "\nExpected AADs:"
|
77
|
+
puts "ModelA AAD: #{aad_a}"
|
78
|
+
puts "ModelB AAD: #{aad_b}"
|
79
|
+
puts "AADs are same: #{aad_a == aad_b}"
|
80
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true'
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Debugging cross-context validation..."
|
9
|
+
|
10
|
+
# Setup encryption keys
|
11
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
12
|
+
Familia.config.encryption_keys = test_keys
|
13
|
+
Familia.config.current_key_version = :v1
|
14
|
+
|
15
|
+
class ModelA < Familia::Horreum
|
16
|
+
feature :encrypted_fields
|
17
|
+
identifier_field :id
|
18
|
+
field :id
|
19
|
+
encrypted_field :api_key
|
20
|
+
end
|
21
|
+
|
22
|
+
class ModelB < Familia::Horreum
|
23
|
+
feature :encrypted_fields
|
24
|
+
identifier_field :id
|
25
|
+
field :id
|
26
|
+
encrypted_field :api_key
|
27
|
+
end
|
28
|
+
|
29
|
+
# Clean database
|
30
|
+
Familia.dbclient.flushdb
|
31
|
+
|
32
|
+
model_a = ModelA.new(id: 'same-id')
|
33
|
+
model_b = ModelB.new(id: 'same-id')
|
34
|
+
|
35
|
+
model_a.api_key = 'secret-key'
|
36
|
+
model_b.api_key = 'secret-key'
|
37
|
+
|
38
|
+
cipher_a = model_a.instance_variable_get(:@api_key)
|
39
|
+
cipher_b = model_b.instance_variable_get(:@api_key)
|
40
|
+
|
41
|
+
puts "cipher_a: #{cipher_a.class} - #{cipher_a.encrypted_value}"
|
42
|
+
puts "cipher_b: #{cipher_b.class} - #{cipher_b.encrypted_value}"
|
43
|
+
|
44
|
+
# Now try cross-context access
|
45
|
+
puts "\n=== Cross-context test ==="
|
46
|
+
model_a.instance_variable_set(:@api_key, cipher_b)
|
47
|
+
puts "Set cipher_b into model_a"
|
48
|
+
|
49
|
+
begin
|
50
|
+
result = model_a.api_key
|
51
|
+
puts "Got result: #{result.class} - should be ConcealedString"
|
52
|
+
|
53
|
+
# Try to reveal it
|
54
|
+
result.reveal do |plaintext|
|
55
|
+
puts "Successfully revealed: #{plaintext}"
|
56
|
+
end
|
57
|
+
puts "ERROR: Cross-context decryption should have failed!"
|
58
|
+
rescue Familia::EncryptionError => e
|
59
|
+
puts "SUCCESS: Got expected encryption error: #{e.message}"
|
60
|
+
rescue => e
|
61
|
+
puts "Got unexpected error: #{e.class}: #{e.message}"
|
62
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
4
|
+
ENV['TEST'] = 'true' # Mark as test environment
|
5
|
+
require 'familia'
|
6
|
+
require_relative 'try/helpers/test_helpers'
|
7
|
+
|
8
|
+
puts "Testing database load vs in-memory encryption..."
|
9
|
+
|
10
|
+
class TestModel < Familia::Horreum
|
11
|
+
feature :encrypted_fields
|
12
|
+
identifier_field :id
|
13
|
+
field :id
|
14
|
+
field :title
|
15
|
+
encrypted_field :secret
|
16
|
+
end
|
17
|
+
|
18
|
+
test_keys = { v1: Base64.strict_encode64('a' * 32) }
|
19
|
+
Familia.config.encryption_keys = test_keys
|
20
|
+
Familia.config.current_key_version = :v1
|
21
|
+
|
22
|
+
# Clean database
|
23
|
+
Familia.dbclient.flushdb
|
24
|
+
|
25
|
+
puts "\n=== PHASE 1: In-memory object ==="
|
26
|
+
model = TestModel.new(id: 'test1')
|
27
|
+
model.title = 'Test Title'
|
28
|
+
puts "PRE-ENCRYPT: model.exists? = #{model.exists?}"
|
29
|
+
model.secret = 'plaintext-secret'
|
30
|
+
|
31
|
+
puts "In-memory secret class: #{model.secret.class}"
|
32
|
+
puts "In-memory secret value: #{model.secret}"
|
33
|
+
|
34
|
+
model.secret.reveal do |plaintext|
|
35
|
+
puts "In-memory reveal: #{plaintext}"
|
36
|
+
end
|
37
|
+
|
38
|
+
puts "\n=== PHASE 2: Save to database ==="
|
39
|
+
model.save
|
40
|
+
|
41
|
+
puts "Keys in database: #{Familia.dbclient.keys('*')}"
|
42
|
+
puts "Hash contents: #{Familia.dbclient.hgetall('testmodel:test1:object')}"
|
43
|
+
|
44
|
+
raw_secret = Familia.dbclient.hget('testmodel:test1:object', 'secret')
|
45
|
+
puts "Raw secret from DB: #{raw_secret}"
|
46
|
+
|
47
|
+
puts "\n=== PHASE 3: Load from database ==="
|
48
|
+
loaded_model = TestModel.load('test1')
|
49
|
+
|
50
|
+
if loaded_model
|
51
|
+
puts "Loaded model exists: #{loaded_model.exists?}"
|
52
|
+
puts "Loaded secret class: #{loaded_model.secret.class}"
|
53
|
+
puts "Loaded secret value: #{loaded_model.secret}"
|
54
|
+
|
55
|
+
begin
|
56
|
+
loaded_model.secret.reveal do |plaintext|
|
57
|
+
puts "Loaded reveal: #{plaintext}"
|
58
|
+
end
|
59
|
+
rescue => e
|
60
|
+
puts "Loaded reveal ERROR: #{e.class}: #{e.message}"
|
61
|
+
end
|
62
|
+
else
|
63
|
+
puts "Failed to load model"
|
64
|
+
end
|