familia 2.0.0.pre15 → 2.0.0.pre17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +624 -20
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +2 -2
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +10 -10
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +3 -3
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/field_groups_try.rb +244 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +443 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +3 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/horreum/settings_try.rb +2 -0
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- data/try/valkey.conf +26 -0
- metadata +92 -52
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -5,15 +5,15 @@ module Familia
|
|
5
5
|
EncryptedData = Data.define(:algorithm, :nonce, :ciphertext, :auth_tag, :key_version) do
|
6
6
|
# Class methods for parsing and validation
|
7
7
|
def self.valid?(json_string)
|
8
|
-
return true if json_string.nil?
|
9
|
-
return false unless json_string.
|
8
|
+
return true if json_string.nil? # Allow nil values
|
9
|
+
return false unless json_string.is_a?(::String)
|
10
10
|
|
11
11
|
begin
|
12
12
|
parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
|
13
13
|
return false unless parsed.is_a?(Hash)
|
14
14
|
|
15
15
|
# Check for required fields
|
16
|
-
required_fields = [
|
16
|
+
required_fields = %i[algorithm nonce ciphertext auth_tag key_version]
|
17
17
|
result = required_fields.all? { |field| parsed.key?(field) }
|
18
18
|
Familia.ld "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}"
|
19
19
|
result
|
@@ -26,9 +26,7 @@ module Familia
|
|
26
26
|
def self.validate!(json_string)
|
27
27
|
return nil if json_string.nil?
|
28
28
|
|
29
|
-
unless json_string.
|
30
|
-
raise EncryptionError, "Expected JSON string, got #{json_string.class}"
|
31
|
-
end
|
29
|
+
raise EncryptionError, "Expected JSON string, got #{json_string.class}" unless json_string.is_a?(::String)
|
32
30
|
|
33
31
|
begin
|
34
32
|
parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
|
@@ -36,16 +34,12 @@ module Familia
|
|
36
34
|
raise EncryptionError, "Invalid JSON structure: #{e.message}"
|
37
35
|
end
|
38
36
|
|
39
|
-
unless parsed.is_a?(Hash)
|
40
|
-
raise EncryptionError, "Expected JSON object, got #{parsed.class}"
|
41
|
-
end
|
37
|
+
raise EncryptionError, "Expected JSON object, got #{parsed.class}" unless parsed.is_a?(Hash)
|
42
38
|
|
43
|
-
required_fields = [
|
39
|
+
required_fields = %i[algorithm nonce ciphertext auth_tag key_version]
|
44
40
|
missing_fields = required_fields.reject { |field| parsed.key?(field) }
|
45
41
|
|
46
|
-
unless missing_fields.empty?
|
47
|
-
raise EncryptionError, "Missing required fields: #{missing_fields.join(', ')}"
|
48
|
-
end
|
42
|
+
raise EncryptionError, "Missing required fields: #{missing_fields.join(', ')}" unless missing_fields.empty?
|
49
43
|
|
50
44
|
new(**parsed)
|
51
45
|
end
|
@@ -77,16 +71,12 @@ module Familia
|
|
77
71
|
end
|
78
72
|
|
79
73
|
def validate_decryptable!
|
80
|
-
unless algorithm
|
81
|
-
raise EncryptionError, "Missing algorithm field"
|
82
|
-
end
|
74
|
+
raise EncryptionError, 'Missing algorithm field' unless algorithm
|
83
75
|
|
84
76
|
# Ensure Registry is set up before checking algorithms
|
85
77
|
Registry.setup! if Registry.providers.empty?
|
86
78
|
|
87
|
-
unless Registry.providers.key?(algorithm)
|
88
|
-
raise EncryptionError, "Unsupported algorithm: #{algorithm}"
|
89
|
-
end
|
79
|
+
raise EncryptionError, "Unsupported algorithm: #{algorithm}" unless Registry.providers.key?(algorithm)
|
90
80
|
|
91
81
|
unless nonce && ciphertext && auth_tag && key_version
|
92
82
|
missing = []
|
@@ -107,22 +97,23 @@ module Familia
|
|
107
97
|
raise EncryptionError, "Invalid nonce size: expected #{provider.nonce_size}, got #{decoded_nonce.bytesize}"
|
108
98
|
end
|
109
99
|
rescue ArgumentError
|
110
|
-
raise EncryptionError,
|
100
|
+
raise EncryptionError, 'Invalid Base64 encoding in nonce field'
|
111
101
|
end
|
112
102
|
|
113
103
|
begin
|
114
|
-
Base64.strict_decode64(ciphertext)
|
104
|
+
Base64.strict_decode64(ciphertext) # ciphertext can be variable size
|
115
105
|
rescue ArgumentError
|
116
|
-
raise EncryptionError,
|
106
|
+
raise EncryptionError, 'Invalid Base64 encoding in ciphertext field'
|
117
107
|
end
|
118
108
|
|
119
109
|
begin
|
120
110
|
decoded_auth_tag = Base64.strict_decode64(auth_tag)
|
121
111
|
if decoded_auth_tag.bytesize != provider.auth_tag_size
|
122
|
-
raise EncryptionError,
|
112
|
+
raise EncryptionError,
|
113
|
+
"Invalid auth_tag size: expected #{provider.auth_tag_size}, got #{decoded_auth_tag.bytesize}"
|
123
114
|
end
|
124
115
|
rescue ArgumentError
|
125
|
-
raise EncryptionError,
|
116
|
+
raise EncryptionError, 'Invalid Base64 encoding in auth_tag field'
|
126
117
|
end
|
127
118
|
|
128
119
|
# Validate that the key version exists
|
@@ -39,7 +39,8 @@ module Familia
|
|
39
39
|
Familia::Encryption.derivation_count.increment
|
40
40
|
|
41
41
|
begin
|
42
|
-
data = Familia::Encryption::EncryptedData.new(**Familia::JsonSerializer.parse(encrypted_json,
|
42
|
+
data = Familia::Encryption::EncryptedData.new(**Familia::JsonSerializer.parse(encrypted_json,
|
43
|
+
symbolize_names: true))
|
43
44
|
|
44
45
|
# Validate algorithm support
|
45
46
|
provider = Registry.get(data.algorithm)
|
@@ -67,15 +68,16 @@ module Familia
|
|
67
68
|
def decode_and_validate(encoded, expected_size, component)
|
68
69
|
decoded = Base64.strict_decode64(encoded)
|
69
70
|
raise EncryptionError, 'Invalid encrypted data' unless decoded.bytesize == expected_size
|
71
|
+
|
70
72
|
decoded
|
71
|
-
rescue ArgumentError
|
73
|
+
rescue ArgumentError
|
72
74
|
raise EncryptionError, "Invalid Base64 encoding in #{component} field"
|
73
75
|
end
|
74
76
|
|
75
77
|
def decode_and_validate_ciphertext(encoded)
|
76
78
|
Base64.strict_decode64(encoded)
|
77
|
-
rescue ArgumentError
|
78
|
-
raise EncryptionError,
|
79
|
+
rescue ArgumentError
|
80
|
+
raise EncryptionError, 'Invalid Base64 encoding in ciphertext field'
|
79
81
|
end
|
80
82
|
|
81
83
|
def derive_key(context, version: nil, provider: nil)
|
@@ -95,9 +95,7 @@ module Familia
|
|
95
95
|
validate_key_length!(master_key)
|
96
96
|
|
97
97
|
raw_personal = personal || Familia.config.encryption_personalization
|
98
|
-
if raw_personal.include?("\0")
|
99
|
-
raise EncryptionError, 'Personalization string must not contain null bytes'
|
100
|
-
end
|
98
|
+
raise EncryptionError, 'Personalization string must not contain null bytes' if raw_personal.include?("\0")
|
101
99
|
|
102
100
|
personal_string = raw_personal.ljust(16, "\0")
|
103
101
|
|
@@ -132,8 +130,8 @@ module Familia
|
|
132
130
|
|
133
131
|
result = {
|
134
132
|
ciphertext: ciphertext_with_tag[0...-16],
|
135
|
-
auth_tag: ciphertext_with_tag[-16
|
136
|
-
nonce: nonce
|
133
|
+
auth_tag: ciphertext_with_tag[-16..],
|
134
|
+
nonce: nonce,
|
137
135
|
}
|
138
136
|
|
139
137
|
# Clear intermediate values
|
@@ -168,10 +166,10 @@ module Familia
|
|
168
166
|
def clear_aead_instance(aead_instance)
|
169
167
|
# Attempt to clear RbNaCl's internal key storage
|
170
168
|
# This is a best-effort cleanup since RbNaCl stores keys as strings internally
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
169
|
+
return unless aead_instance.instance_variable_defined?(:@key)
|
170
|
+
|
171
|
+
internal_key = aead_instance.instance_variable_get(:@key)
|
172
|
+
secure_wipe(internal_key) if internal_key
|
175
173
|
end
|
176
174
|
|
177
175
|
def validate_key_length!(key)
|
@@ -53,8 +53,8 @@ module Familia
|
|
53
53
|
|
54
54
|
{
|
55
55
|
ciphertext: ciphertext_with_tag[0...-16],
|
56
|
-
auth_tag: ciphertext_with_tag[-16
|
57
|
-
nonce: nonce
|
56
|
+
auth_tag: ciphertext_with_tag[-16..],
|
57
|
+
nonce: nonce,
|
58
58
|
}
|
59
59
|
end
|
60
60
|
|
@@ -88,9 +88,8 @@ module Familia
|
|
88
88
|
def derive_key(master_key, context, personal: nil)
|
89
89
|
validate_key_length!(master_key)
|
90
90
|
raw_personal = personal || Familia.config.encryption_personalization
|
91
|
-
if raw_personal.include?("\0")
|
92
|
-
|
93
|
-
end
|
91
|
+
raise EncryptionError, 'Personalization string must not contain null bytes' if raw_personal.include?("\0")
|
92
|
+
|
94
93
|
personal_string = raw_personal.ljust(16, "\0")
|
95
94
|
|
96
95
|
RbNaCl::Hash.blake2b(
|
@@ -18,8 +18,8 @@ module Familia
|
|
18
18
|
class << self
|
19
19
|
# Enable request-scoped caching (opt-in for performance)
|
20
20
|
def with_request_cache
|
21
|
-
|
22
|
-
|
21
|
+
Fiber[:familia_request_cache_enabled] = true
|
22
|
+
Fiber[:familia_request_cache] = {}
|
23
23
|
yield
|
24
24
|
ensure
|
25
25
|
clear_request_cache!
|
@@ -27,12 +27,12 @@ module Familia
|
|
27
27
|
|
28
28
|
# Clear all cached keys and disable caching
|
29
29
|
def clear_request_cache!
|
30
|
-
if (cache =
|
30
|
+
if (cache = Fiber[:familia_request_cache])
|
31
31
|
cache.each_value { |key| secure_wipe(key) }
|
32
32
|
cache.clear
|
33
33
|
end
|
34
|
-
|
35
|
-
|
34
|
+
Fiber[:familia_request_cache_enabled] = false
|
35
|
+
Fiber[:familia_request_cache] = nil
|
36
36
|
end
|
37
37
|
|
38
38
|
private
|
@@ -43,8 +43,8 @@ module Familia
|
|
43
43
|
master_key = get_master_key(version)
|
44
44
|
|
45
45
|
# Only use cache if explicitly enabled for this request
|
46
|
-
if
|
47
|
-
cache =
|
46
|
+
if Fiber[:familia_request_cache_enabled]
|
47
|
+
cache = Fiber[:familia_request_cache] ||= {}
|
48
48
|
cache_key = "#{version}:#{context}"
|
49
49
|
|
50
50
|
# Return cached key if available (within same request only)
|
data/lib/familia/encryption.rb
CHANGED
@@ -16,7 +16,6 @@ module Familia
|
|
16
16
|
class EncryptionError < StandardError; end
|
17
17
|
|
18
18
|
module Encryption
|
19
|
-
|
20
19
|
# Smart facade with provider selection and field-specific encryption
|
21
20
|
#
|
22
21
|
# Usage in EncryptedFieldType can now be more flexible:
|
@@ -109,7 +108,7 @@ module Familia
|
|
109
108
|
preferred_available: Registry.default_provider&.class&.name,
|
110
109
|
using_hardware: hardware_acceleration?,
|
111
110
|
key_versions: encryption_keys.keys,
|
112
|
-
current_version: current_key_version
|
111
|
+
current_version: current_key_version,
|
113
112
|
}
|
114
113
|
end
|
115
114
|
|
@@ -140,7 +139,7 @@ module Familia
|
|
140
139
|
results[algo] = {
|
141
140
|
time: time,
|
142
141
|
ops_per_sec: (iterations * 2 / time).round,
|
143
|
-
priority: provider_class.priority
|
142
|
+
priority: provider_class.priority,
|
144
143
|
}
|
145
144
|
end
|
146
145
|
|
data/lib/familia/errors.rb
CHANGED
@@ -10,7 +10,10 @@ module Familia
|
|
10
10
|
|
11
11
|
class SerializerError < Problem; end
|
12
12
|
|
13
|
-
|
13
|
+
# Raised when attempting to start transactions or pipelines on connection types that don't support them
|
14
|
+
class OperationModeError < Problem; end
|
15
|
+
|
16
|
+
class NotDistinguishableError < Problem
|
14
17
|
attr_reader :value
|
15
18
|
|
16
19
|
def initialize(value)
|
@@ -19,7 +22,7 @@ module Familia
|
|
19
22
|
end
|
20
23
|
|
21
24
|
def message
|
22
|
-
"
|
25
|
+
"Cannot represent #{value}<#{value.class}> as a string"
|
23
26
|
end
|
24
27
|
end
|
25
28
|
|
@@ -36,9 +39,12 @@ module Familia
|
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
39
|
-
#
|
42
|
+
# UnsortedSet Familia.connection_provider or use middleware to provide connections.
|
40
43
|
class NoConnectionAvailable < Problem; end
|
41
44
|
|
45
|
+
# Raised when a load method fails to find the requested object
|
46
|
+
class NotFound < Problem; end
|
47
|
+
|
42
48
|
# Raised when attempting to refresh an object whose key doesn't exist in the database
|
43
49
|
class KeyNotFoundError < NonUniqueKey
|
44
50
|
attr_reader :key
|
@@ -1,12 +1,13 @@
|
|
1
1
|
# lib/familia/features/autoloader.rb
|
2
2
|
|
3
|
+
# rubocop:disable Style/ClassAndModuleChildren
|
3
4
|
module Familia::Features
|
4
5
|
# Provides autoloading functionality for Ruby files based on patterns and conventions.
|
5
6
|
#
|
6
7
|
# Used by the Features module at library startup to load feature files, and available
|
7
8
|
# as a utility for other modules requiring file autoloading capabilities.
|
8
9
|
module Autoloader
|
9
|
-
using Familia::Refinements::
|
10
|
+
using Familia::Refinements::StylizeWords
|
10
11
|
|
11
12
|
# Autoloads feature files when this module is included.
|
12
13
|
#
|
@@ -15,25 +16,22 @@ module Familia::Features
|
|
15
16
|
#
|
16
17
|
# @param base [Module] the module including this autoloader
|
17
18
|
def self.included(base)
|
18
|
-
|
19
19
|
# Get the directory where the including module is defined
|
20
20
|
# This should be lib/familia for the Features module
|
21
21
|
base_path = File.dirname(caller_locations(1, 1).first.path)
|
22
|
-
|
22
|
+
config_name = normalize_to_config_name(base.name)
|
23
|
+
|
23
24
|
dir_patterns = [
|
24
25
|
File.join(base_path, 'features', '*.rb'),
|
25
|
-
File.join(base_path,
|
26
|
-
File.join(base_path,
|
26
|
+
File.join(base_path, config_name, 'features', '*.rb'),
|
27
|
+
File.join(base_path, config_name, 'features.rb'),
|
27
28
|
]
|
28
29
|
|
29
30
|
# Ensure the Features module exists within the base module
|
30
|
-
unless base.const_defined?(:Features) ||
|
31
|
-
base.const_set(:Features, Module.new)
|
32
|
-
end
|
33
|
-
|
31
|
+
base.const_set(:Features, Module.new) unless base.const_defined?(:Features) || config_name.eql?('features')
|
34
32
|
|
35
33
|
# Use the shared autoload_files method
|
36
|
-
autoload_files(dir_patterns, log_prefix: "Autoloader[#{
|
34
|
+
autoload_files(dir_patterns, log_prefix: "Autoloader[#{config_name}]")
|
37
35
|
end
|
38
36
|
|
39
37
|
# Autoloads Ruby files matching the given patterns.
|
@@ -45,17 +43,37 @@ module Familia::Features
|
|
45
43
|
patterns = Array(patterns)
|
46
44
|
|
47
45
|
patterns.each do |pattern|
|
48
|
-
Familia.
|
46
|
+
Familia.trace :AUTOLOAD, nil, "[#{log_prefix}] Autoloader loading features from #{pattern}"
|
49
47
|
Dir.glob(pattern).each do |file_path|
|
50
48
|
basename = File.basename(file_path)
|
51
49
|
|
52
50
|
# Skip excluded files
|
53
51
|
next if exclude.include?(basename)
|
54
52
|
|
55
|
-
Familia.trace :FEATURE, nil, "[#{log_prefix}] Loading #{
|
53
|
+
Familia.trace :FEATURE, nil, "[#{log_prefix}] Loading #{basename}" if Familia.debug?
|
56
54
|
require File.expand_path(file_path)
|
57
55
|
end
|
58
56
|
end
|
59
57
|
end
|
58
|
+
|
59
|
+
class << self
|
60
|
+
# Converts the value into a string that can be used to look up configuration
|
61
|
+
# values or system paths. This replicates the normalization done by the
|
62
|
+
# Familia::Horreum model class config_name method.
|
63
|
+
#
|
64
|
+
# @see Familia::Horreum::DefinitionMethods#config_name
|
65
|
+
#
|
66
|
+
# NOTE: We don't call that existing method directly b/c Autoloader is meant
|
67
|
+
# to work for any class/module that matches `dir_patterns` (see `included`).
|
68
|
+
#
|
69
|
+
# @param value [String] the value to normalize (typically a class name)
|
70
|
+
# @return [String] the underscored value as a string
|
71
|
+
def normalize_to_config_name(value)
|
72
|
+
return nil if value.nil?
|
73
|
+
|
74
|
+
value.demodularize.snake_case
|
75
|
+
end
|
76
|
+
end
|
60
77
|
end
|
61
78
|
end
|
79
|
+
# rubocop:enable Style/ClassAndModuleChildren
|
@@ -161,8 +161,8 @@ class ConcealedString
|
|
161
161
|
# Prevent accidental exposure through string conversion and serialization
|
162
162
|
#
|
163
163
|
# Ruby has two string conversion methods with different purposes:
|
164
|
-
# - to_s: explicit conversion (obj.to_s
|
165
|
-
# - to_str: implicit coercion (File.read(obj)
|
164
|
+
# - to_s: explicit conversion (`obj.to_s`, string interpolation `"#{obj}"`)
|
165
|
+
# - to_str: implicit coercion (`File.read(obj)`, `"prefix" + obj`)
|
166
166
|
#
|
167
167
|
# We implement to_s for safe logging/debugging but deliberately omit to_str
|
168
168
|
# to prevent encrypted data from being used where strings are expected.
|
@@ -268,7 +268,7 @@ class ConcealedString
|
|
268
268
|
|
269
269
|
# Prevent exposure in JSON serialization - fail closed for security
|
270
270
|
def to_json(*)
|
271
|
-
raise Familia::SerializerError,
|
271
|
+
raise Familia::SerializerError, 'ConcealedString cannot be serialized to JSON'
|
272
272
|
end
|
273
273
|
|
274
274
|
# Prevent exposure in Rails serialization (as_json -> to_json)
|
@@ -291,5 +291,4 @@ class ConcealedString
|
|
291
291
|
def encrypted_json?(data)
|
292
292
|
Familia::Encryption::EncryptedData.valid?(data)
|
293
293
|
end
|
294
|
-
|
295
294
|
end
|
@@ -58,7 +58,7 @@ module Familia
|
|
58
58
|
|
59
59
|
# If we have a raw string (from direct instance variable manipulation),
|
60
60
|
# wrap it in ConcealedString which will trigger validation
|
61
|
-
if concealed.
|
61
|
+
if concealed.is_a?(::String) && !concealed.is_a?(ConcealedString)
|
62
62
|
# This happens when someone directly sets the instance variable
|
63
63
|
# (e.g., during tampering tests). Wrapping in ConcealedString
|
64
64
|
# will trigger validate_decryptable! and catch invalid algorithms
|
@@ -75,7 +75,8 @@ module Familia
|
|
75
75
|
# Context validation: detect cross-context attacks
|
76
76
|
# Only validate if we have a proper ConcealedString instance
|
77
77
|
if concealed.is_a?(ConcealedString) && !concealed.belongs_to_context?(self, field_name)
|
78
|
-
raise Familia::EncryptionError,
|
78
|
+
raise Familia::EncryptionError,
|
79
|
+
"Context isolation violation: encrypted field '#{field_name}' does not belong to #{self.class.name}:#{identifier}"
|
79
80
|
end
|
80
81
|
|
81
82
|
concealed
|
@@ -90,13 +91,13 @@ module Familia
|
|
90
91
|
field_name = @name
|
91
92
|
method_name = @method_name
|
92
93
|
fast_method_name = @fast_method_name
|
93
|
-
|
94
|
+
self
|
94
95
|
|
95
96
|
handle_method_conflict(klass, fast_method_name) do
|
96
97
|
klass.define_method fast_method_name do |val|
|
97
98
|
raise ArgumentError, "#{fast_method_name} requires a value" if val.nil?
|
98
99
|
|
99
|
-
#
|
100
|
+
# UnsortedSet via the setter method to get proper ConcealedString wrapping
|
100
101
|
send(:"#{method_name}=", val) if method_name
|
101
102
|
|
102
103
|
# Get the ConcealedString and extract encrypted data for storage
|
@@ -203,18 +204,16 @@ module Familia
|
|
203
204
|
if @aad_fields.empty?
|
204
205
|
# When no AAD fields specified, use class:field:identifier
|
205
206
|
base_components.join(':')
|
206
|
-
|
207
|
+
elsif record.exists?
|
207
208
|
# For unsaved records, don't enforce AAD fields since they can change
|
208
209
|
# For saved records, include field values for tamper protection
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
base_components.join(':')
|
217
|
-
end
|
210
|
+
values = @aad_fields.map { |field| record.send(field) }
|
211
|
+
all_components = [*base_components, *values].compact
|
212
|
+
Digest::SHA256.hexdigest(all_components.join(':'))
|
213
|
+
# Include specified field values in AAD for persisted records
|
214
|
+
else
|
215
|
+
# For unsaved records, only use class:field:identifier for context isolation
|
216
|
+
base_components.join(':')
|
218
217
|
end
|
219
218
|
end
|
220
219
|
end
|