familia 2.0.0.pre19 → 2.0.0.pre21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +4 -9
- data/.github/workflows/code-smells.yml +64 -3
- data/.pre-commit-config.yaml +8 -6
- data/.reek.yml +10 -9
- data/.rubocop.yml +4 -0
- data/CHANGELOG.rst +177 -112
- data/CLAUDE.md +28 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +20 -17
- data/bin/try +16 -0
- data/bin/tryouts +16 -0
- data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
- data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
- data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
- data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
- data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
- data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
- data/docs/1106-participates_in-bidirectional-solution.md +129 -0
- data/docs/guides/encryption.md +486 -0
- data/docs/guides/feature-encrypted-fields.md +123 -7
- data/docs/guides/feature-expiration.md +161 -117
- data/docs/guides/feature-external-identifiers.md +415 -443
- data/docs/guides/feature-object-identifiers.md +400 -269
- data/docs/guides/feature-quantization.md +120 -6
- data/docs/guides/feature-relationships-indexing.md +318 -0
- data/docs/guides/feature-relationships-methods.md +146 -604
- data/docs/guides/feature-relationships-participation.md +263 -0
- data/docs/guides/feature-relationships.md +118 -136
- data/docs/guides/feature-system-devs.md +176 -693
- data/docs/guides/feature-system.md +119 -6
- data/docs/guides/feature-transient-fields.md +81 -0
- data/docs/guides/field-system.md +778 -0
- data/docs/guides/index.md +32 -15
- data/docs/guides/logging.md +187 -0
- data/docs/guides/optimized-loading.md +674 -0
- data/docs/guides/thread-safety-monitoring.md +61 -0
- data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
- data/docs/migrating/v2.0.0-pre22.md +241 -0
- data/docs/overview.md +7 -9
- data/docs/reference/api-technical.md +267 -320
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
- data/examples/autoloader/mega_customer.rb +2 -0
- data/examples/datatype_standalone.rb +4 -3
- data/examples/encrypted_fields.rb +2 -1
- data/examples/json_usage_patterns.rb +2 -0
- data/examples/relationships.rb +3 -0
- data/examples/safe_dump.rb +2 -1
- data/examples/sampling_demo.rb +53 -0
- data/examples/single_connection_transaction_confusions.rb +2 -1
- data/familia.gemspec +2 -1
- data/lib/familia/base.rb +2 -0
- data/lib/familia/connection/behavior.rb +2 -0
- data/lib/familia/connection/handlers.rb +2 -0
- data/lib/familia/connection/individual_command_proxy.rb +2 -0
- data/lib/familia/connection/middleware.rb +34 -24
- data/lib/familia/connection/operation_core.rb +2 -0
- data/lib/familia/connection/operations.rb +2 -0
- data/lib/familia/connection/pipelined_core.rb +2 -0
- data/lib/familia/connection/transaction_core.rb +68 -0
- data/lib/familia/connection.rb +18 -3
- data/lib/familia/data_type/class_methods.rb +3 -1
- data/lib/familia/data_type/connection.rb +2 -0
- data/lib/familia/data_type/database_commands.rb +2 -0
- data/lib/familia/data_type/serialization.rb +6 -4
- data/lib/familia/data_type/settings.rb +2 -0
- data/lib/familia/data_type/types/counter.rb +2 -0
- data/lib/familia/data_type/types/hashkey.rb +7 -5
- data/lib/familia/data_type/types/listkey.rb +2 -0
- data/lib/familia/data_type/types/lock.rb +2 -0
- data/lib/familia/data_type/types/sorted_set.rb +2 -0
- data/lib/familia/data_type/types/stringkey.rb +2 -0
- data/lib/familia/data_type/types/unsorted_set.rb +2 -0
- data/lib/familia/data_type.rb +2 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -2
- data/lib/familia/encryption/manager.rb +2 -0
- data/lib/familia/encryption/provider.rb +2 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/registry.rb +2 -0
- data/lib/familia/encryption/request_cache.rb +2 -0
- data/lib/familia/encryption.rb +9 -2
- data/lib/familia/errors.rb +2 -0
- data/lib/familia/features/autoloader.rb +2 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
- data/lib/familia/features/encrypted_fields.rb +2 -2
- data/lib/familia/features/expiration/extensions.rb +3 -1
- data/lib/familia/features/expiration.rb +12 -4
- data/lib/familia/features/external_identifier.rb +33 -7
- data/lib/familia/features/object_identifier.rb +2 -0
- data/lib/familia/features/quantization.rb +3 -1
- data/lib/familia/features/relationships/README.md +3 -1
- data/lib/familia/features/relationships/collection_operations.rb +2 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +138 -9
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +89 -21
- data/lib/familia/features/relationships/indexing.rb +3 -0
- data/lib/familia/features/relationships/indexing_relationship.rb +3 -1
- data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
- data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
- data/lib/familia/features/relationships/participation.rb +155 -69
- data/lib/familia/features/relationships/participation_membership.rb +69 -0
- data/lib/familia/features/relationships/participation_relationship.rb +34 -6
- data/lib/familia/features/relationships/score_encoding.rb +2 -0
- data/lib/familia/features/relationships.rb +5 -3
- data/lib/familia/features/safe_dump.rb +2 -0
- data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
- data/lib/familia/features/transient_fields.rb +2 -0
- data/lib/familia/features.rb +2 -0
- data/lib/familia/field_type.rb +3 -1
- data/lib/familia/horreum/connection.rb +17 -1
- data/lib/familia/horreum/database_commands.rb +2 -0
- data/lib/familia/horreum/definition.rb +16 -6
- data/lib/familia/horreum/management.rb +212 -42
- data/lib/familia/horreum/persistence.rb +176 -108
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum/serialization.rb +23 -4
- data/lib/familia/horreum/settings.rb +2 -0
- data/lib/familia/horreum/utils.rb +2 -0
- data/lib/familia/horreum.rb +15 -1
- data/lib/familia/identifier_extractor.rb +2 -0
- data/lib/familia/instrumentation.rb +156 -0
- data/lib/familia/json_serializer.rb +2 -0
- data/lib/familia/logging.rb +92 -32
- data/lib/familia/refinements/dear_json.rb +2 -0
- data/lib/familia/refinements/stylize_words.rb +2 -14
- data/lib/familia/refinements/time_literals.rb +2 -0
- data/lib/familia/refinements.rb +2 -0
- data/lib/familia/secure_identifier.rb +10 -2
- data/lib/familia/settings.rb +2 -0
- data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
- data/lib/familia/thread_safety/monitor.rb +328 -0
- data/lib/familia/utils.rb +13 -0
- data/lib/familia/verifiable_identifier.rb +3 -1
- data/lib/familia/version.rb +3 -1
- data/lib/familia.rb +31 -4
- data/lib/middleware/database_command_counter.rb +152 -0
- data/lib/middleware/database_logger.rb +295 -170
- data/lib/multi_result.rb +2 -0
- data/try/edge_cases/empty_identifiers_try.rb +2 -0
- data/try/edge_cases/hash_symbolization_try.rb +2 -0
- data/try/edge_cases/json_serialization_try.rb +2 -0
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
- data/try/edge_cases/race_conditions_try.rb +4 -0
- data/try/edge_cases/reserved_keywords_try.rb +4 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/edge_cases/ttl_side_effects_try.rb +4 -0
- data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
- data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
- data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
- data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
- data/try/features/encrypted_fields/memory_security_try.rb +4 -0
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
- data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
- data/try/features/encryption/config_persistence_try.rb +4 -0
- data/try/features/encryption/core_try.rb +4 -0
- data/try/features/encryption/instance_variable_scope_try.rb +4 -0
- data/try/features/encryption/module_loading_try.rb +4 -0
- data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
- data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
- data/try/features/encryption/roundtrip_validation_try.rb +4 -0
- data/try/features/encryption/secure_memory_handling_try.rb +4 -0
- data/try/features/expiration/expiration_try.rb +4 -0
- data/try/features/external_identifier/external_identifier_try.rb +171 -8
- data/try/features/feature_dependencies_try.rb +2 -0
- data/try/features/feature_improvements_try.rb +2 -0
- data/try/features/field_groups_try.rb +2 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
- data/try/features/object_identifier/object_identifier_try.rb +2 -0
- data/try/features/quantization/quantization_try.rb +4 -0
- data/try/features/real_feature_integration_try.rb +2 -0
- data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
- data/try/features/relationships/indexing_rebuild_try.rb +600 -0
- data/try/features/relationships/indexing_try.rb +2 -0
- data/try/features/relationships/participation_bidirectional_try.rb +242 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
- data/try/features/relationships/participation_commands_verification_try.rb +2 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
- data/try/features/relationships/participation_reverse_index_try.rb +15 -13
- data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
- data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
- data/try/features/relationships/relationships_api_changes_try.rb +2 -0
- data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
- data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
- data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
- data/try/features/relationships/relationships_performance_try.rb +4 -0
- data/try/features/relationships/relationships_performance_working_try.rb +4 -0
- data/try/features/relationships/relationships_try.rb +6 -4
- data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
- data/try/features/safe_dump/safe_dump_try.rb +4 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/refresh_reset_try.rb +3 -0
- data/try/features/transient_fields/simple_refresh_test.rb +3 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
- data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
- data/try/integration/connection/fiber_context_preservation_try.rb +4 -0
- data/try/integration/connection/handler_constraints_try.rb +4 -0
- data/try/integration/connection/isolated_dbclient_try.rb +4 -0
- data/try/integration/connection/middleware_reconnect_try.rb +2 -0
- data/try/integration/connection/operation_mode_guards_try.rb +4 -0
- data/try/integration/connection/pipeline_fallback_integration_try.rb +3 -0
- data/try/integration/connection/pools_try.rb +4 -0
- data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
- data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
- data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
- data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
- data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
- data/try/integration/connection/transaction_modes_try.rb +4 -0
- data/try/integration/conventional_inheritance_try.rb +4 -0
- data/try/integration/create_method_try.rb +4 -0
- data/try/integration/cross_component_try.rb +4 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +4 -0
- data/try/integration/data_types/datatype_transactions_try.rb +4 -0
- data/try/integration/database_consistency_try.rb +4 -0
- data/try/integration/familia_extended_try.rb +4 -0
- data/try/integration/familia_members_methods_try.rb +4 -0
- data/try/integration/models/customer_safe_dump_try.rb +4 -0
- data/try/integration/models/customer_try.rb +4 -0
- data/try/integration/models/datatype_base_try.rb +4 -0
- data/try/integration/models/familia_object_try.rb +4 -0
- data/try/integration/persistence_operations_try.rb +4 -0
- data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
- data/try/integration/save_methods_consistency_try.rb +241 -0
- data/try/integration/scenarios_try.rb +4 -0
- data/try/integration/secure_identifier_try.rb +4 -0
- data/try/integration/transaction_safety_core_try.rb +176 -0
- data/try/integration/transaction_safety_workflow_try.rb +291 -0
- data/try/integration/verifiable_identifier_try.rb +4 -0
- data/try/investigation/pipeline_routing/README.md +228 -0
- data/try/performance/benchmarks_try.rb +4 -0
- data/try/performance/transaction_safety_benchmark_try.rb +238 -0
- data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
- data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
- data/try/support/debugging/cache_behavior_tracer.rb +4 -0
- data/try/support/debugging/debug_aad_process.rb +3 -0
- data/try/support/debugging/debug_concealed_internal.rb +3 -0
- data/try/support/debugging/debug_concealed_reveal.rb +3 -0
- data/try/support/debugging/debug_context_aad.rb +3 -0
- data/try/support/debugging/debug_context_simple.rb +3 -0
- data/try/support/debugging/debug_cross_context.rb +3 -0
- data/try/support/debugging/debug_database_load.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
- data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
- data/try/support/debugging/debug_field_decrypt.rb +3 -0
- data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
- data/try/support/debugging/debug_load_path.rb +3 -0
- data/try/support/debugging/debug_method_definition.rb +3 -0
- data/try/support/debugging/debug_method_resolution.rb +3 -0
- data/try/support/debugging/debug_minimal.rb +3 -0
- data/try/support/debugging/debug_provider.rb +3 -0
- data/try/support/debugging/debug_secure_behavior.rb +3 -0
- data/try/support/debugging/debug_string_class.rb +3 -0
- data/try/support/debugging/debug_test.rb +3 -0
- data/try/support/debugging/debug_test_design.rb +3 -0
- data/try/support/debugging/encryption_method_tracer.rb +4 -0
- data/try/support/debugging/provider_diagnostics.rb +4 -0
- data/try/support/helpers/test_cleanup.rb +4 -0
- data/try/support/helpers/test_helpers.rb +5 -0
- data/try/support/memory/memory_basic_test.rb +4 -0
- data/try/support/memory/memory_detailed_test.rb +4 -0
- data/try/support/memory/memory_search_for_string.rb +4 -0
- data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
- data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
- data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
- data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
- data/try/support/prototypes/atomic_saves_v4.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
- data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
- data/try/support/prototypes/pooling/pool_siege.rb +4 -2
- data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
- data/try/thread_safety/README.md +496 -0
- data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
- data/try/thread_safety/connection_chain_race_try.rb +148 -0
- data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
- data/try/thread_safety/feature_registry_race_try.rb +226 -0
- data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
- data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
- data/try/thread_safety/field_registration_race_try.rb +222 -0
- data/try/thread_safety/logger_initialization_race_try.rb +170 -0
- data/try/thread_safety/middleware_registration_race_try.rb +154 -0
- data/try/thread_safety/module_config_race_try.rb +175 -0
- data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
- data/try/unit/core/autoloader_try.rb +4 -0
- data/try/unit/core/base_enhancements_try.rb +4 -0
- data/try/unit/core/connection_try.rb +4 -0
- data/try/unit/core/errors_try.rb +4 -0
- data/try/unit/core/extensions_try.rb +4 -0
- data/try/unit/core/familia_logger_try.rb +2 -0
- data/try/unit/core/familia_try.rb +4 -0
- data/try/unit/core/middleware_sampling_try.rb +335 -0
- data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
- data/try/unit/core/middleware_thread_safety_try.rb +245 -0
- data/try/unit/core/middleware_try.rb +4 -0
- data/try/unit/core/settings_try.rb +4 -0
- data/try/unit/core/time_utils_try.rb +4 -0
- data/try/unit/core/tools_try.rb +4 -0
- data/try/unit/core/utils_try.rb +37 -0
- data/try/unit/data_types/boolean_try.rb +4 -0
- data/try/unit/data_types/counter_try.rb +4 -0
- data/try/unit/data_types/datatype_base_try.rb +4 -0
- data/try/unit/data_types/hash_try.rb +4 -0
- data/try/unit/data_types/list_try.rb +4 -0
- data/try/unit/data_types/lock_try.rb +4 -0
- data/try/unit/data_types/sorted_set_try.rb +4 -0
- data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
- data/try/unit/data_types/string_try.rb +4 -0
- data/try/unit/data_types/unsortedset_try.rb +4 -0
- data/try/unit/familia_resolve_class_try.rb +116 -0
- data/try/unit/horreum/auto_indexing_on_save_try.rb +5 -1
- data/try/unit/horreum/automatic_index_validation_try.rb +2 -0
- data/try/unit/horreum/base_try.rb +4 -0
- data/try/unit/horreum/class_methods_try.rb +4 -0
- data/try/unit/horreum/commands_try.rb +4 -0
- data/try/unit/horreum/defensive_initialization_try.rb +4 -0
- data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
- data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
- data/try/unit/horreum/field_categories_try.rb +4 -0
- data/try/unit/horreum/field_definition_try.rb +4 -0
- data/try/unit/horreum/initialization_try.rb +4 -0
- data/try/unit/horreum/json_type_preservation_try.rb +2 -0
- data/try/unit/horreum/optimized_loading_try.rb +156 -0
- data/try/unit/horreum/relations_try.rb +4 -0
- data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
- data/try/unit/horreum/serialization_try.rb +4 -0
- data/try/unit/horreum/settings_try.rb +4 -0
- data/try/unit/horreum/unique_index_edge_cases_try.rb +4 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +2 -0
- data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
- data/try/unit/middleware/database_logger_methods_try.rb +251 -0
- data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
- data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
- data/try/unit/thread_safety_monitor_try.rb +149 -0
- metadata +72 -17
- data/.github/workflows/code-quality.yml +0 -138
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +0 -91
- data/changelog.d/20251011_203905_delano_next.rst +0 -30
- data/changelog.d/20251011_212633_delano_next.rst +0 -13
- data/changelog.d/20251011_221253_delano_next.rst +0 -26
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
- data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
- data/docs/archive/FAMILIA_UPDATE.md +0 -226
- data/docs/archive/README.md +0 -64
- data/docs/archive/api-reference.md +0 -333
- data/docs/guides/core-field-system.md +0 -806
- data/docs/guides/implementation.md +0 -276
- data/docs/guides/security-model.md +0 -183
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
The Naming Problem
|
|
2
|
+
|
|
3
|
+
bidirectional: true is misleading because:
|
|
4
|
+
|
|
5
|
+
1. It's not truly bidirectional - It only helps you manage membership in specific instances, not query all memberships
|
|
6
|
+
2. Better name would be: generate_participant_methods: true
|
|
7
|
+
3. True bidirectionality would mean both sides can easily query their relationships
|
|
8
|
+
|
|
9
|
+
What True Bidirectionality Should Look Like
|
|
10
|
+
|
|
11
|
+
Option 1: Auto-generate reverse collections
|
|
12
|
+
|
|
13
|
+
class Customer < Familia::Horreum
|
|
14
|
+
participates_in Team, :members, bidirectional: true, reverse: :teams
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Should generate:
|
|
18
|
+
customer.teams # All teams this customer is in
|
|
19
|
+
customer.teams.count # How many teams
|
|
20
|
+
customer.teams.include?(team_id) # Check membership
|
|
21
|
+
|
|
22
|
+
Option 2: Make bidirectional actually bidirectional
|
|
23
|
+
|
|
24
|
+
class Customer < Familia::Horreum
|
|
25
|
+
participates_in Team, :members, bidirectional: true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Should auto-generate (using pluralized class name):
|
|
29
|
+
customer.teams # Since we're participating in Team class
|
|
30
|
+
customer.organizations # If also participating in Organization class
|
|
31
|
+
|
|
32
|
+
Current Implementation Gap
|
|
33
|
+
|
|
34
|
+
Looking at the actual usage pattern:
|
|
35
|
+
# Easy to go from Team → Customers
|
|
36
|
+
team.members.to_a # Simple!
|
|
37
|
+
customers = Customer.multiget(*team.members.to_a)
|
|
38
|
+
|
|
39
|
+
# Hard to go from Customer → Teams
|
|
40
|
+
customer.participations.members
|
|
41
|
+
.select { |k| k.start_with?("team:") }
|
|
42
|
+
.map { |k| k.split(':')[1] }
|
|
43
|
+
# ... etc - complicated!
|
|
44
|
+
|
|
45
|
+
What's Really Happening
|
|
46
|
+
|
|
47
|
+
The bidirectional flag only controls whether these instance-to-instance methods are generated:
|
|
48
|
+
- customer.add_to_team_members(specific_team)
|
|
49
|
+
- customer.in_team_members?(specific_team)
|
|
50
|
+
|
|
51
|
+
It does NOT create instance-to-collection methods:
|
|
52
|
+
- customer.teams ❌
|
|
53
|
+
- customer.all_team_memberships ❌
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
The functionality we are hoping to achieve:
|
|
57
|
+
Bidirectional Relationships Feature Spec
|
|
58
|
+
|
|
59
|
+
Problem
|
|
60
|
+
|
|
61
|
+
Currently, Familia relationships are asymmetric. While you can easily query team.members to get all members, there's no convenient way to get
|
|
62
|
+
all teams a user belongs to without manually parsing the participations reverse index.
|
|
63
|
+
|
|
64
|
+
Solution
|
|
65
|
+
|
|
66
|
+
Auto-generate reverse collection methods on participant classes to provide symmetric access to relationships.
|
|
67
|
+
|
|
68
|
+
## Implemented API (Using _instance Suffix Pattern)
|
|
69
|
+
|
|
70
|
+
class User < Familia::Horreum
|
|
71
|
+
participates_in Team, :members # Auto-generates: user.team_instances
|
|
72
|
+
participates_in Team, :admins # Also adds to: user.team_instances (union)
|
|
73
|
+
participates_in Organization, :employees # Auto-generates: user.organization_instances
|
|
74
|
+
participates_in Organization, :contractors, as: :contracting_orgs # Custom name
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Forward (existing)
|
|
78
|
+
team.members # → SortedSet of user IDs
|
|
79
|
+
team.add_members_instance(user) # → Adds to collection + tracks participation
|
|
80
|
+
team.remove_members_instance(user) # → Removes from collection + untracks
|
|
81
|
+
|
|
82
|
+
# Reverse (NEW)
|
|
83
|
+
user.team_instances # → Array of Team instances user belongs to
|
|
84
|
+
user.team_ids # → Array of team IDs (efficient, no loading)
|
|
85
|
+
user.team? # → Boolean: belongs to any teams?
|
|
86
|
+
user.team_count # → Count without loading objects
|
|
87
|
+
|
|
88
|
+
# Custom naming (user chooses base name via as: parameter)
|
|
89
|
+
user.contracting_orgs_instances # → Array of Organization instances
|
|
90
|
+
user.contracting_orgs_ids # → Array of IDs
|
|
91
|
+
user.contracting_orgs? # → Boolean
|
|
92
|
+
user.contracting_orgs_count # → Count
|
|
93
|
+
|
|
94
|
+
## Naming Rationale: Why `_instance` Suffix?
|
|
95
|
+
|
|
96
|
+
The implementation uses an `_instance` suffix pattern instead of pluralization/singularization to avoid fragility:
|
|
97
|
+
|
|
98
|
+
**Target Methods (Forward Direction):**
|
|
99
|
+
- `team.add_members_instance(user)` instead of `team.add_member(user)`
|
|
100
|
+
- `team.remove_members_instance(user)` instead of `team.remove_member(user)`
|
|
101
|
+
|
|
102
|
+
**Reverse Collection Methods:**
|
|
103
|
+
- `user.team_instances` instead of `user.teams`
|
|
104
|
+
- `user.organization_instances` instead of `user.organizations`
|
|
105
|
+
|
|
106
|
+
**Benefits:**
|
|
107
|
+
1. **No irregular plurals** - Avoids issues with words like "person/people", "child/children", "foot/feet"
|
|
108
|
+
2. **Clear intent** - The suffix makes it obvious you're working with instances, not counts or IDs
|
|
109
|
+
3. **Consistent pattern** - Same suffix for both forward and reverse operations
|
|
110
|
+
4. **No external dependencies** - Removes need for inflection libraries like `dry-inflector`
|
|
111
|
+
5. **Predictable** - Easy to remember and document
|
|
112
|
+
|
|
113
|
+
**Trade-off:**
|
|
114
|
+
- Slightly more verbose, but eliminates an entire class of edge case bugs
|
|
115
|
+
|
|
116
|
+
Key Requirements
|
|
117
|
+
|
|
118
|
+
1. Automatic generation - No manual method definitions needed
|
|
119
|
+
2. Multiple collections - Union of all collections to same target class
|
|
120
|
+
3. Performance - Efficient ID-only access without loading objects (no caching for data freshness)
|
|
121
|
+
4. Custom naming - Override auto-generated names when needed via `as:` parameter
|
|
122
|
+
5. Thread-safe - No caching means no stale data or cache invalidation complexity
|
|
123
|
+
|
|
124
|
+
Benefits
|
|
125
|
+
|
|
126
|
+
- Symmetry - Both directions equally convenient
|
|
127
|
+
- Discoverability - Natural Ruby method names
|
|
128
|
+
- Efficiency - Choose between full objects, IDs, or counts
|
|
129
|
+
- Backwards compatible - All existing code continues to work
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# Implementation Guide
|
|
2
|
+
|
|
3
|
+
## Architecture Overview
|
|
4
|
+
|
|
5
|
+
The encrypted fields feature uses a modular provider system with field transformation hooks:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
User Input → Field Setter → Provider Selection → Encryption → Valkey/Redis
|
|
9
|
+
Valkey/Redis → Algorithm Detection → Decryption → Field Getter → User Output
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Provider Architecture
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
16
|
+
│ Manager │ │ Registry │ │ Providers │
|
|
17
|
+
│ │ │ │ │ │
|
|
18
|
+
│ - encrypt() │───→│ - get() │───→│ XChaCha20Poly │
|
|
19
|
+
│ - decrypt() │ │ - register() │ │ AES-GCM │
|
|
20
|
+
│ - derive_key() │ │ - available() │ │ │
|
|
21
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Core Components
|
|
25
|
+
|
|
26
|
+
### 1. Registry System
|
|
27
|
+
|
|
28
|
+
The Registry manages available encryption providers and selects the best one:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
module Familia::Encryption::Registry
|
|
32
|
+
# Auto-register available providers by priority
|
|
33
|
+
def self.setup!
|
|
34
|
+
|
|
35
|
+
# Get provider instance by algorithm
|
|
36
|
+
def self.get(algorithm)
|
|
37
|
+
|
|
38
|
+
# Get highest-priority available provider
|
|
39
|
+
def self.default_provider
|
|
40
|
+
|
|
41
|
+
# Get available algorithm names
|
|
42
|
+
def self.available_algorithms
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Manager Class
|
|
47
|
+
|
|
48
|
+
The Manager handles encryption/decryption operations with provider delegation:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
class Familia::Encryption::Manager
|
|
52
|
+
# Use specific algorithm or auto-select best
|
|
53
|
+
def initialize(algorithm: nil)
|
|
54
|
+
|
|
55
|
+
# Encrypt with context-specific key derivation
|
|
56
|
+
def encrypt(plaintext, context:, additional_data: nil)
|
|
57
|
+
|
|
58
|
+
# Decrypt with automatic algorithm detection
|
|
59
|
+
def decrypt(encrypted_json, context:, additional_data: nil)
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Provider Interface
|
|
64
|
+
|
|
65
|
+
All providers implement a common interface:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class Provider
|
|
69
|
+
ALGORITHM = 'algorithm-name'
|
|
70
|
+
NONCE_SIZE = 12 # or 24 for XChaCha20
|
|
71
|
+
AUTH_TAG_SIZE = 16
|
|
72
|
+
|
|
73
|
+
def self.available? # Check if dependencies are met
|
|
74
|
+
def self.priority # Higher = preferred (XChaCha20: 100, AES: 50)
|
|
75
|
+
|
|
76
|
+
def encrypt(plaintext, key, additional_data)
|
|
77
|
+
def decrypt(ciphertext, key, nonce, auth_tag, additional_data)
|
|
78
|
+
def derive_key(master_key, context, personal: nil)
|
|
79
|
+
def generate_nonce
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. Key Derivation
|
|
84
|
+
|
|
85
|
+
Each field gets a unique encryption key using provider-specific methods:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Master Key + Field Context → Provider KDF → Field-Specific Key
|
|
89
|
+
|
|
90
|
+
XChaCha20-Poly1305: BLAKE2b with personalization
|
|
91
|
+
AES-256-GCM: HKDF-SHA256
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Implementation Steps
|
|
95
|
+
|
|
96
|
+
### Step 1: Enable Encryption
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
class MyModel < Familia::Horreum
|
|
100
|
+
# Add the feature
|
|
101
|
+
feature :encrypted_fields
|
|
102
|
+
|
|
103
|
+
# Define encrypted fields
|
|
104
|
+
encrypted_field :sensitive_data
|
|
105
|
+
encrypted_field :api_key
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Step 2: Configure Keys
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# config/initializers/familia.rb
|
|
113
|
+
Familia.configure do |config|
|
|
114
|
+
config.encryption_keys = {
|
|
115
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY_V1']
|
|
116
|
+
}
|
|
117
|
+
config.current_key_version = :v1
|
|
118
|
+
config.encryption_personalization = 'MyApp-2024' # Optional
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Validate configuration at startup
|
|
122
|
+
Familia::Encryption.validate_configuration!
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Step 3: Generate Keys
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Generate a secure 256-bit key (32 bytes)
|
|
129
|
+
$ openssl rand -base64 32
|
|
130
|
+
# => base64_encoded_key_here
|
|
131
|
+
|
|
132
|
+
# Add to environment
|
|
133
|
+
$ echo "FAMILIA_ENCRYPTION_KEY_V1=base64_encoded_key_here" >> .env
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Step 4: Install Optional Dependencies
|
|
137
|
+
|
|
138
|
+
For best security and performance, install RbNaCl:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Add to Gemfile
|
|
142
|
+
gem 'rbnacl', '~> 7.1', '>= 7.1.1'
|
|
143
|
+
|
|
144
|
+
# Install
|
|
145
|
+
$ bundle install
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Without RbNaCl, Familia falls back to OpenSSL AES-256-GCM (still secure but lower priority).
|
|
149
|
+
|
|
150
|
+
## Advanced Usage
|
|
151
|
+
|
|
152
|
+
### Additional Authenticated Data (AAD)
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
class SecureDocument < Familia::Horreum
|
|
156
|
+
feature :encrypted_fields
|
|
157
|
+
|
|
158
|
+
field :doc_id, :owner_id, :classification
|
|
159
|
+
encrypted_field :content, aad_fields: [:doc_id, :owner_id, :classification]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The content can only be decrypted if doc_id, owner_id, and classification
|
|
163
|
+
# values match those used during encryption
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Request-Level Caching
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# For performance optimization
|
|
170
|
+
Familia::Encryption.with_request_cache do
|
|
171
|
+
vault.secret_key = "value1"
|
|
172
|
+
vault.api_token = "value2"
|
|
173
|
+
vault.save # Reuses derived keys within this block
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Cache is automatically cleared when block exits
|
|
177
|
+
# Or manually: Familia::Encryption.clear_request_cache!
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### ConcealedString Objects
|
|
181
|
+
|
|
182
|
+
Encrypted fields return ConcealedString objects to prevent accidental exposure:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
secret = vault.secret_key
|
|
186
|
+
secret.class # => ConcealedString
|
|
187
|
+
puts secret # => "[CONCEALED]" (automatic redaction)
|
|
188
|
+
secret.inspect # => "[CONCEALED]" (automatic redaction)
|
|
189
|
+
|
|
190
|
+
# Safe access pattern - requires explicit reveal
|
|
191
|
+
secret.reveal do |raw_value|
|
|
192
|
+
# Use raw_value carefully - avoid creating copies
|
|
193
|
+
HTTP.post('/api', headers: { 'X-Token' => raw_value })
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Check if cleared from memory
|
|
197
|
+
secret.cleared? # Returns true if wiped
|
|
198
|
+
|
|
199
|
+
# Explicit cleanup
|
|
200
|
+
secret.clear! # Best-effort memory wiping
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Provider-Specific Features
|
|
204
|
+
|
|
205
|
+
### XChaCha20-Poly1305 Provider (Recommended)
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# Enable with RbNaCl gem
|
|
209
|
+
gem 'rbnacl', '~> 7.1'
|
|
210
|
+
|
|
211
|
+
# Benefits:
|
|
212
|
+
# - Extended nonce (192 bits vs 96 bits)
|
|
213
|
+
# - Better resistance to nonce reuse
|
|
214
|
+
# - BLAKE2b key derivation with personalization
|
|
215
|
+
# - Priority: 100 (highest)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### AES-256-GCM Provider (Fallback)
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
# Always available with OpenSSL
|
|
222
|
+
# - 256-bit keys, 96-bit nonces (12 bytes)
|
|
223
|
+
# - HKDF-SHA256 key derivation
|
|
224
|
+
# - Priority: 50
|
|
225
|
+
# - Good compatibility, proven security
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Performance Optimization
|
|
229
|
+
|
|
230
|
+
### Provider Benchmarking
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
# Compare provider performance
|
|
234
|
+
results = Familia::Encryption.benchmark(iterations: 1000)
|
|
235
|
+
puts results
|
|
236
|
+
# => {
|
|
237
|
+
# "xchacha20poly1305" => { time: 0.45, ops_per_sec: 4444, priority: 100 },
|
|
238
|
+
# "aes-256-gcm" => { time: 0.52, ops_per_sec: 3846, priority: 50 }
|
|
239
|
+
# }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Monitoring Key Derivation
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
# Monitor key derivations (should increment with each operation)
|
|
246
|
+
puts Familia::Encryption.derivation_count.value
|
|
247
|
+
# => 42
|
|
248
|
+
|
|
249
|
+
# Reset counter for testing
|
|
250
|
+
Familia::Encryption.reset_derivation_count!
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Encryption Status
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# Get current encryption setup info
|
|
257
|
+
status = Familia::Encryption.status
|
|
258
|
+
# => {
|
|
259
|
+
# default_algorithm: "xchacha20poly1305",
|
|
260
|
+
# available_algorithms: ["xchacha20poly1305", "aes-256-gcm"],
|
|
261
|
+
# preferred_available: "Familia::Encryption::Providers::XChaCha20Poly1305Provider",
|
|
262
|
+
# using_hardware: false,
|
|
263
|
+
# key_versions: [:v1, :v2],
|
|
264
|
+
# current_version: :v2
|
|
265
|
+
# }
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Field-Level Features
|
|
269
|
+
|
|
270
|
+
### Instance Methods
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
vault = Vault.new(secret_key: 'secret', api_token: 'token123')
|
|
274
|
+
|
|
275
|
+
# Check if any encrypted fields have values
|
|
276
|
+
vault.encrypted_data? # => true
|
|
277
|
+
|
|
278
|
+
# Clear all encrypted field values from memory
|
|
279
|
+
vault.clear_encrypted_fields!
|
|
280
|
+
|
|
281
|
+
# Check if all encrypted fields have been cleared
|
|
282
|
+
vault.encrypted_fields_cleared? # => true
|
|
283
|
+
|
|
284
|
+
# Re-encrypt all fields with current settings (for key rotation)
|
|
285
|
+
vault.re_encrypt_fields!
|
|
286
|
+
vault.save
|
|
287
|
+
|
|
288
|
+
# Get encryption status for all encrypted fields
|
|
289
|
+
status = vault.encrypted_fields_status
|
|
290
|
+
# => {
|
|
291
|
+
# secret_key: { encrypted: true, algorithm: "xchacha20poly1305", cleared: false },
|
|
292
|
+
# api_token: { encrypted: true, cleared: true }
|
|
293
|
+
# }
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Class Methods
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
class Vault < Familia::Horreum
|
|
300
|
+
feature :encrypted_fields
|
|
301
|
+
encrypted_field :secret_key
|
|
302
|
+
encrypted_field :api_token
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Get list of encrypted field names
|
|
306
|
+
Vault.encrypted_fields # => [:secret_key, :api_token]
|
|
307
|
+
|
|
308
|
+
# Check if a field is encrypted
|
|
309
|
+
Vault.encrypted_field?(:secret_key) # => true
|
|
310
|
+
Vault.encrypted_field?(:name) # => false
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Key Rotation
|
|
314
|
+
|
|
315
|
+
The feature supports key versioning for seamless key rotation:
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
# Step 1: Add new key version while keeping old keys
|
|
319
|
+
Familia.configure do |config|
|
|
320
|
+
config.encryption_keys = {
|
|
321
|
+
v1: old_key,
|
|
322
|
+
v2: new_key
|
|
323
|
+
}
|
|
324
|
+
config.current_key_version = :v2
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Step 2: Objects decrypt with any valid key, encrypt with current key
|
|
328
|
+
vault.secret_key = "new-secret" # Encrypted with v2 key
|
|
329
|
+
vault.save
|
|
330
|
+
|
|
331
|
+
# Step 3: Re-encrypt existing records
|
|
332
|
+
vault.re_encrypt_fields! # Uses current key version
|
|
333
|
+
vault.save
|
|
334
|
+
|
|
335
|
+
# Step 4: After all data is re-encrypted, remove old key
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Error Handling
|
|
339
|
+
|
|
340
|
+
The feature provides specific error types for different failure modes:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
# Invalid ciphertext or tampering
|
|
344
|
+
begin
|
|
345
|
+
vault.secret_key.reveal { |s| s }
|
|
346
|
+
rescue Familia::EncryptionError => e
|
|
347
|
+
# "Decryption failed - invalid key or corrupted data"
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Missing encryption configuration
|
|
351
|
+
Familia.config.encryption_keys = {}
|
|
352
|
+
begin
|
|
353
|
+
vault.secret_key.reveal { |s| s }
|
|
354
|
+
rescue Familia::EncryptionError => e
|
|
355
|
+
# "No encryption keys configured"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Invalid key version
|
|
359
|
+
begin
|
|
360
|
+
vault.secret_key.reveal { |s| s }
|
|
361
|
+
rescue Familia::EncryptionError => e
|
|
362
|
+
# "No key for version: v1"
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Testing
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
# Test helper setup
|
|
370
|
+
Familia.config.encryption_keys = { v1: Base64.strict_encode64('a' * 32) }
|
|
371
|
+
Familia.config.current_key_version = :v1
|
|
372
|
+
|
|
373
|
+
# In tests
|
|
374
|
+
it "encrypts sensitive fields" do
|
|
375
|
+
user = User.create(api_token: "secret-token")
|
|
376
|
+
|
|
377
|
+
# Verify encryption in Redis
|
|
378
|
+
raw_value = redis.hget(user.dbkey, "api_token")
|
|
379
|
+
expect(raw_value).not_to include("secret-token")
|
|
380
|
+
|
|
381
|
+
encrypted_data = JSON.parse(raw_value)
|
|
382
|
+
expect(encrypted_data).to have_key("ciphertext")
|
|
383
|
+
expect(encrypted_data).to have_key("algorithm")
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
it "provides concealed string access" do
|
|
387
|
+
user = User.create(api_token: "secret-token")
|
|
388
|
+
concealed = user.api_token
|
|
389
|
+
|
|
390
|
+
expect(concealed).to be_a(ConcealedString)
|
|
391
|
+
expect(concealed.to_s).to eq("[CONCEALED]")
|
|
392
|
+
|
|
393
|
+
concealed.reveal do |token|
|
|
394
|
+
expect(token).to eq("secret-token")
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Security Model
|
|
400
|
+
|
|
401
|
+
### Ciphertext Format
|
|
402
|
+
|
|
403
|
+
Encrypted data is stored as JSON with algorithm-specific metadata:
|
|
404
|
+
|
|
405
|
+
```json
|
|
406
|
+
{
|
|
407
|
+
"algorithm": "xchacha20poly1305",
|
|
408
|
+
"nonce": "base64_encoded_nonce",
|
|
409
|
+
"ciphertext": "base64_encoded_data",
|
|
410
|
+
"auth_tag": "base64_encoded_tag",
|
|
411
|
+
"key_version": "v1"
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Memory Safety Limitations
|
|
416
|
+
|
|
417
|
+
⚠️ **Important**: Ruby provides NO memory safety guarantees:
|
|
418
|
+
- No secure memory wiping (best-effort only)
|
|
419
|
+
- Garbage collector may copy secrets
|
|
420
|
+
- String operations create uncontrolled copies
|
|
421
|
+
- Memory dumps may contain plaintext secrets
|
|
422
|
+
|
|
423
|
+
For highly sensitive applications, consider:
|
|
424
|
+
- External key management (HashiCorp Vault, AWS KMS)
|
|
425
|
+
- Hardware Security Modules (HSMs)
|
|
426
|
+
- Languages with secure memory handling
|
|
427
|
+
- Dedicated cryptographic appliances
|
|
428
|
+
|
|
429
|
+
### Threat Model
|
|
430
|
+
|
|
431
|
+
✅ **Protected Against:**
|
|
432
|
+
- Database compromise (encrypted data only)
|
|
433
|
+
- Field value swapping (field-specific keys)
|
|
434
|
+
- Cross-record attacks (record-specific keys)
|
|
435
|
+
- Tampering (authenticated encryption)
|
|
436
|
+
|
|
437
|
+
❌ **Not Protected Against:**
|
|
438
|
+
- Master key compromise (all data compromised)
|
|
439
|
+
- Application memory compromise (plaintext in RAM)
|
|
440
|
+
- Side-channel attacks (timing, power analysis)
|
|
441
|
+
- Insider threats with application access
|
|
442
|
+
|
|
443
|
+
## Troubleshooting
|
|
444
|
+
|
|
445
|
+
### Common Issues
|
|
446
|
+
|
|
447
|
+
1. **"No encryption key configured"**
|
|
448
|
+
- Ensure `FAMILIA_ENCRYPTION_KEY` is set
|
|
449
|
+
- Check `Familia.config.encryption_keys`
|
|
450
|
+
|
|
451
|
+
2. **"Decryption failed"**
|
|
452
|
+
- Verify correct key version
|
|
453
|
+
- Check if data was encrypted with different key
|
|
454
|
+
- Ensure AAD fields haven't changed
|
|
455
|
+
|
|
456
|
+
3. **Performance degradation**
|
|
457
|
+
- Enable request-level caching with `with_request_cache`
|
|
458
|
+
- Consider installing RbNaCl gem for XChaCha20
|
|
459
|
+
|
|
460
|
+
4. **Provider not available**
|
|
461
|
+
- Install RbNaCl for XChaCha20: `gem install rbnacl`
|
|
462
|
+
- Falls back to AES-256-GCM automatically
|
|
463
|
+
|
|
464
|
+
## API Reference
|
|
465
|
+
|
|
466
|
+
### Module Methods
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
# Main encryption/decryption
|
|
470
|
+
Familia::Encryption.encrypt(plaintext, context:, additional_data: nil)
|
|
471
|
+
Familia::Encryption.decrypt(encrypted_json, context:, additional_data: nil)
|
|
472
|
+
Familia::Encryption.encrypt_with(algorithm, plaintext, context:, additional_data: nil)
|
|
473
|
+
|
|
474
|
+
# Configuration and status
|
|
475
|
+
Familia::Encryption.validate_configuration!
|
|
476
|
+
Familia::Encryption.status
|
|
477
|
+
Familia::Encryption.benchmark(iterations: 1000)
|
|
478
|
+
|
|
479
|
+
# Request caching
|
|
480
|
+
Familia::Encryption.with_request_cache { block }
|
|
481
|
+
Familia::Encryption.clear_request_cache!
|
|
482
|
+
|
|
483
|
+
# Monitoring
|
|
484
|
+
Familia::Encryption.derivation_count
|
|
485
|
+
Familia::Encryption.reset_derivation_count!
|
|
486
|
+
```
|