familia 2.0.0.pre4 → 2.0.0.pre6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +11 -8
- data/Gemfile +5 -1
- data/Gemfile.lock +19 -3
- data/README.md +36 -157
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +347 -0
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +600 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +106 -0
- data/docs/wiki/Implementation-Guide.md +276 -0
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +183 -0
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +18 -27
- data/lib/familia/connection.rb +6 -5
- data/lib/familia/{datatype → data_type}/commands.rb +2 -5
- data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +12 -14
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +119 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
- data/lib/familia/encryption/registry.rb +50 -0
- data/lib/familia/encryption.rb +178 -0
- data/lib/familia/encryption_request_cache.rb +68 -0
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
- data/lib/familia/features/encrypted_fields.rb +28 -0
- data/lib/familia/features/expiration.rb +107 -77
- data/lib/familia/features/quantization.rb +5 -9
- data/lib/familia/features/relatable_objects.rb +2 -4
- data/lib/familia/features/safe_dump.rb +14 -17
- data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
- data/lib/familia/features/transient_fields.rb +47 -0
- data/lib/familia/features.rb +40 -24
- data/lib/familia/field_type.rb +273 -0
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
- data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
- data/lib/familia/horreum/subclass/definition.rb +469 -0
- data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +30 -22
- data/lib/familia/logging.rb +14 -14
- data/lib/familia/settings.rb +39 -3
- data/lib/familia/utils.rb +45 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +3 -2
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -5
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +1 -2
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/core/pools_try.rb +2 -2
- data/try/core/secure_identifier_try.rb +0 -1
- data/try/core/settings_try.rb +0 -1
- data/try/core/utils_try.rb +0 -1
- data/try/{datatypes → data_types}/boolean_try.rb +1 -2
- data/try/data_types/counter_try.rb +93 -0
- data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
- data/try/{datatypes → data_types}/hash_try.rb +1 -2
- data/try/{datatypes → data_types}/list_try.rb +1 -2
- data/try/data_types/lock_try.rb +133 -0
- data/try/{datatypes → data_types}/set_try.rb +1 -2
- data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
- data/try/{datatypes → data_types}/string_try.rb +1 -2
- data/try/debugging/README.md +32 -0
- data/try/debugging/cache_behavior_tracer.rb +91 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/debugging/encryption_method_tracer.rb +138 -0
- data/try/debugging/provider_diagnostics.rb +110 -0
- data/try/edge_cases/hash_symbolization_try.rb +0 -1
- data/try/edge_cases/json_serialization_try.rb +0 -1
- data/try/edge_cases/reserved_keywords_try.rb +42 -11
- data/try/encryption/config_persistence_try.rb +192 -0
- data/try/encryption/encryption_core_try.rb +328 -0
- data/try/encryption/instance_variable_scope_try.rb +31 -0
- data/try/encryption/module_loading_try.rb +28 -0
- data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
- data/try/encryption/roundtrip_validation_try.rb +28 -0
- data/try/encryption/secure_memory_handling_try.rb +125 -0
- data/try/features/encrypted_fields_core_try.rb +125 -0
- data/try/features/encrypted_fields_integration_try.rb +216 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
- data/try/features/encrypted_fields_security_try.rb +377 -0
- data/try/features/encryption_fields/aad_protection_try.rb +138 -0
- data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +141 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
- data/try/features/encryption_fields/fresh_key_try.rb +168 -0
- data/try/features/encryption_fields/key_rotation_try.rb +123 -0
- data/try/features/encryption_fields/memory_security_try.rb +37 -0
- data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/expiration_try.rb +0 -1
- data/try/features/feature_dependencies_try.rb +159 -0
- data/try/features/quantization_try.rb +0 -1
- data/try/features/real_feature_integration_try.rb +148 -0
- data/try/features/relatable_objects_try.rb +0 -1
- data/try/features/safe_dump_advanced_try.rb +0 -1
- data/try/features/safe_dump_try.rb +0 -1
- data/try/features/transient_fields/redacted_string_try.rb +248 -0
- data/try/features/transient_fields/refresh_reset_try.rb +164 -0
- data/try/features/transient_fields/simple_refresh_test.rb +50 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
- data/try/features/transient_fields_core_try.rb +181 -0
- data/try/features/transient_fields_integration_try.rb +260 -0
- data/try/helpers/test_helpers.rb +67 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
- data/try/horreum/field_categories_try.rb +118 -0
- data/try/horreum/field_definition_try.rb +96 -0
- data/try/horreum/initialization_try.rb +1 -2
- data/try/horreum/relations_try.rb +1 -2
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +41 -7
- data/try/memory/memory_basic_test.rb +73 -0
- data/try/memory/memory_detailed_test.rb +121 -0
- data/try/memory/memory_docker_ruby_dump.sh +80 -0
- data/try/memory/memory_search_for_string.rb +83 -0
- data/try/memory/test_actual_redactedstring_protection.rb +38 -0
- data/try/models/customer_safe_dump_try.rb +1 -2
- data/try/models/customer_try.rb +1 -2
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +131 -23
- data/lib/familia/horreum/serialization.rb +0 -445
@@ -0,0 +1,123 @@
|
|
1
|
+
# lib/familia/encryption/providers/aes_gcm_provider.rb
|
2
|
+
|
3
|
+
# ⚠️ RUBY MEMORY SAFETY WARNING ⚠️
|
4
|
+
#
|
5
|
+
# This encryption provider, like all Ruby-based cryptographic implementations,
|
6
|
+
# stores secrets (keys, plaintext, derived keys) as Ruby strings in memory.
|
7
|
+
#
|
8
|
+
# SECURITY IMPLICATIONS:
|
9
|
+
# - Keys remain in memory after use (garbage collection timing is unpredictable)
|
10
|
+
# - Ruby strings cannot be securely wiped from memory
|
11
|
+
# - Memory dumps may contain cryptographic secrets
|
12
|
+
# - Swap files may persist secrets to disk
|
13
|
+
# - String operations create copies that persist in memory
|
14
|
+
#
|
15
|
+
# Ruby provides NO memory safety guarantees for cryptographic secrets.
|
16
|
+
#
|
17
|
+
# For production systems handling sensitive data, consider:
|
18
|
+
# - Hardware Security Modules (HSMs)
|
19
|
+
# - External key management services
|
20
|
+
# - Languages with manual memory management
|
21
|
+
# - Cryptographic appliances with secure memory
|
22
|
+
|
23
|
+
module Familia
|
24
|
+
module Encryption
|
25
|
+
module Providers
|
26
|
+
class AESGCMProvider < Provider
|
27
|
+
ALGORITHM = 'aes-256-gcm'.freeze
|
28
|
+
NONCE_SIZE = 12
|
29
|
+
AUTH_TAG_SIZE = 16
|
30
|
+
|
31
|
+
def self.available?
|
32
|
+
true # OpenSSL is always available
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.priority
|
36
|
+
50 # Fallback option
|
37
|
+
end
|
38
|
+
|
39
|
+
def encrypt(plaintext, key, additional_data = nil)
|
40
|
+
validate_key_length!(key)
|
41
|
+
nonce = generate_nonce
|
42
|
+
cipher = create_cipher(:encrypt)
|
43
|
+
cipher.key = key
|
44
|
+
cipher.iv = nonce
|
45
|
+
cipher.auth_data = additional_data.to_s if additional_data
|
46
|
+
|
47
|
+
ciphertext = cipher.update(plaintext.to_s) + cipher.final
|
48
|
+
|
49
|
+
{
|
50
|
+
ciphertext: ciphertext,
|
51
|
+
auth_tag: cipher.auth_tag,
|
52
|
+
nonce: nonce
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def decrypt(ciphertext, key, nonce, auth_tag, additional_data = nil)
|
57
|
+
validate_key_length!(key)
|
58
|
+
cipher = create_cipher(:decrypt)
|
59
|
+
cipher.key = key
|
60
|
+
cipher.iv = nonce
|
61
|
+
cipher.auth_tag = auth_tag
|
62
|
+
cipher.auth_data = additional_data.to_s if additional_data
|
63
|
+
|
64
|
+
cipher.update(ciphertext) + cipher.final
|
65
|
+
rescue OpenSSL::Cipher::CipherError
|
66
|
+
raise EncryptionError, 'Decryption failed - invalid key or corrupted data'
|
67
|
+
end
|
68
|
+
|
69
|
+
def generate_nonce
|
70
|
+
OpenSSL::Random.random_bytes(NONCE_SIZE)
|
71
|
+
end
|
72
|
+
|
73
|
+
def derive_key(master_key, context, personal: nil)
|
74
|
+
validate_key_length!(master_key)
|
75
|
+
info = personal ? "#{context}:#{personal}" : context
|
76
|
+
OpenSSL::KDF.hkdf(
|
77
|
+
master_key,
|
78
|
+
salt: 'FamiliaEncryption',
|
79
|
+
info: info,
|
80
|
+
length: 32,
|
81
|
+
hash: 'SHA256'
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Clear key from memory (no security guarantees in Ruby)
|
86
|
+
def secure_wipe(key)
|
87
|
+
key&.clear
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.nonce_size
|
91
|
+
NONCE_SIZE
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.auth_tag_size
|
95
|
+
AUTH_TAG_SIZE
|
96
|
+
end
|
97
|
+
|
98
|
+
def nonce_size
|
99
|
+
NONCE_SIZE
|
100
|
+
end
|
101
|
+
|
102
|
+
def auth_tag_size
|
103
|
+
AUTH_TAG_SIZE
|
104
|
+
end
|
105
|
+
|
106
|
+
def algorithm
|
107
|
+
ALGORITHM
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def create_cipher(mode)
|
113
|
+
OpenSSL::Cipher.new('aes-256-gcm').tap { |c| c.public_send(mode) }
|
114
|
+
end
|
115
|
+
|
116
|
+
def validate_key_length!(key)
|
117
|
+
raise EncryptionError, 'Key cannot be nil' if key.nil?
|
118
|
+
raise EncryptionError, 'Key must be at least 32 bytes' if key.bytesize < 32
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb
|
2
|
+
|
3
|
+
# ⚠️ PROTOTYPE IMPLEMENTATION - NOT FOR PRODUCTION USE ⚠️
|
4
|
+
#
|
5
|
+
# This provider is a PROTOTYPE demonstrating alternate memory security practices
|
6
|
+
# for handling secrets in Ruby. It is NOT intended for use with actual sensitive
|
7
|
+
# data or production systems.
|
8
|
+
#
|
9
|
+
# LIMITATIONS:
|
10
|
+
# - Still relies on Ruby strings internally (unavoidable language constraint)
|
11
|
+
# - RbNaCl library stores keys as strings regardless of our efforts
|
12
|
+
# - Ruby's garbage collector behavior cannot be fully controlled
|
13
|
+
# - No guarantee of complete memory cleanup
|
14
|
+
#
|
15
|
+
# This implementation serves as:
|
16
|
+
# - Educational example of security-conscious programming
|
17
|
+
# - Research prototype for future FFI-based implementations
|
18
|
+
# - Demonstration of defense-in-depth techniques
|
19
|
+
#
|
20
|
+
# For actual cryptographic applications, consider:
|
21
|
+
# - Hardware Security Modules (HSMs)
|
22
|
+
# - Dedicated cryptographic appliances
|
23
|
+
# - Languages with manual memory management (C, Rust)
|
24
|
+
# - External key management services
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'rbnacl'
|
28
|
+
require 'ffi'
|
29
|
+
rescue LoadError
|
30
|
+
# Dependencies not available - provider will report as unavailable
|
31
|
+
end
|
32
|
+
|
33
|
+
module Familia
|
34
|
+
module Encryption
|
35
|
+
module Providers
|
36
|
+
# Enhanced XChaCha20Poly1305Provider with improved memory security
|
37
|
+
#
|
38
|
+
# While complete avoidance of Ruby strings for secrets is challenging due to
|
39
|
+
# RbNaCl's internal implementation, this provider implements several security
|
40
|
+
# improvements:
|
41
|
+
#
|
42
|
+
# 1. Minimizes key lifetime in memory
|
43
|
+
# 2. Uses immediate secure wiping after operations
|
44
|
+
# 3. Avoids unnecessary key duplication
|
45
|
+
# 4. Uses locked memory where possible (future enhancement)
|
46
|
+
#
|
47
|
+
class SecureXChaCha20Poly1305Provider < Provider
|
48
|
+
ALGORITHM = 'xchacha20poly1305-secure'.freeze
|
49
|
+
NONCE_SIZE = 24
|
50
|
+
AUTH_TAG_SIZE = 16
|
51
|
+
|
52
|
+
def self.available?
|
53
|
+
!!defined?(RbNaCl) && !!defined?(FFI)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.priority
|
57
|
+
110 # Higher than regular XChaCha20Poly1305Provider
|
58
|
+
end
|
59
|
+
|
60
|
+
def encrypt(plaintext, key, additional_data = nil)
|
61
|
+
validate_key_length!(key)
|
62
|
+
|
63
|
+
# Generate nonce first to avoid holding onto key longer than necessary
|
64
|
+
nonce = generate_nonce
|
65
|
+
|
66
|
+
# Minimize key exposure by performing operation immediately
|
67
|
+
result = perform_encryption(plaintext, key, nonce, additional_data)
|
68
|
+
|
69
|
+
# Attempt to clear the key parameter (if mutable)
|
70
|
+
secure_wipe(key)
|
71
|
+
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
def decrypt(ciphertext, key, nonce, auth_tag, additional_data = nil)
|
76
|
+
validate_key_length!(key)
|
77
|
+
|
78
|
+
# Minimize key exposure by performing operation immediately
|
79
|
+
begin
|
80
|
+
result = perform_decryption(ciphertext, key, nonce, auth_tag, additional_data)
|
81
|
+
ensure
|
82
|
+
# Attempt to clear the key parameter (if mutable)
|
83
|
+
secure_wipe(key)
|
84
|
+
end
|
85
|
+
|
86
|
+
result
|
87
|
+
end
|
88
|
+
|
89
|
+
def generate_nonce
|
90
|
+
RbNaCl::Random.random_bytes(NONCE_SIZE)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Enhanced key derivation with immediate cleanup
|
94
|
+
def derive_key(master_key, context, personal: nil)
|
95
|
+
validate_key_length!(master_key)
|
96
|
+
|
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
|
101
|
+
|
102
|
+
personal_string = raw_personal.ljust(16, "\0")
|
103
|
+
|
104
|
+
# Perform derivation and immediately clear intermediate values
|
105
|
+
derived_key = RbNaCl::Hash.blake2b(
|
106
|
+
context.force_encoding('BINARY'),
|
107
|
+
key: master_key,
|
108
|
+
digest_size: 32,
|
109
|
+
personal: personal_string
|
110
|
+
)
|
111
|
+
|
112
|
+
# Clear personalization string from memory
|
113
|
+
personal_string.clear
|
114
|
+
|
115
|
+
# Return derived key (caller responsible for secure cleanup)
|
116
|
+
derived_key
|
117
|
+
end
|
118
|
+
|
119
|
+
# Clear key from memory (still no security guarantees in Ruby)
|
120
|
+
def secure_wipe(key)
|
121
|
+
key&.clear
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def perform_encryption(plaintext, key, nonce, additional_data)
|
127
|
+
# Create AEAD instance (this internally copies the key)
|
128
|
+
box = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(key)
|
129
|
+
|
130
|
+
aad = additional_data.to_s
|
131
|
+
ciphertext_with_tag = box.encrypt(nonce, plaintext.to_s, aad)
|
132
|
+
|
133
|
+
result = {
|
134
|
+
ciphertext: ciphertext_with_tag[0...-16],
|
135
|
+
auth_tag: ciphertext_with_tag[-16..-1],
|
136
|
+
nonce: nonce
|
137
|
+
}
|
138
|
+
|
139
|
+
# Clear intermediate values
|
140
|
+
ciphertext_with_tag.clear
|
141
|
+
|
142
|
+
result
|
143
|
+
ensure
|
144
|
+
# Clear the AEAD instance's internal key if possible
|
145
|
+
clear_aead_instance(box) if box
|
146
|
+
end
|
147
|
+
|
148
|
+
def perform_decryption(ciphertext, key, nonce, auth_tag, additional_data)
|
149
|
+
# Create AEAD instance (this internally copies the key)
|
150
|
+
box = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(key)
|
151
|
+
|
152
|
+
ciphertext_with_tag = ciphertext + auth_tag
|
153
|
+
aad = additional_data.to_s
|
154
|
+
|
155
|
+
result = box.decrypt(nonce, ciphertext_with_tag, aad)
|
156
|
+
|
157
|
+
# Clear intermediate values
|
158
|
+
ciphertext_with_tag.clear
|
159
|
+
|
160
|
+
result
|
161
|
+
rescue RbNaCl::CryptoError
|
162
|
+
raise EncryptionError, 'Decryption failed - invalid key or corrupted data'
|
163
|
+
ensure
|
164
|
+
# Clear the AEAD instance's internal key if possible
|
165
|
+
clear_aead_instance(box) if box
|
166
|
+
end
|
167
|
+
|
168
|
+
def clear_aead_instance(aead_instance)
|
169
|
+
# Attempt to clear RbNaCl's internal key storage
|
170
|
+
# This is a best-effort cleanup since RbNaCl stores keys as strings internally
|
171
|
+
if aead_instance.instance_variable_defined?(:@key)
|
172
|
+
internal_key = aead_instance.instance_variable_get(:@key)
|
173
|
+
secure_wipe(internal_key) if internal_key
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def validate_key_length!(key)
|
178
|
+
raise EncryptionError, 'Key cannot be nil' if key.nil?
|
179
|
+
raise EncryptionError, 'Key must be at least 32 bytes' if key.bytesize < 32
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# lib/familia/encryption/providers/xchacha20_poly1305_provider.rb
|
2
|
+
|
3
|
+
# ⚠️ RUBY MEMORY SAFETY WARNING ⚠️
|
4
|
+
#
|
5
|
+
# This encryption provider, like all Ruby-based cryptographic implementations,
|
6
|
+
# stores secrets (keys, plaintext, derived keys) as Ruby strings in memory.
|
7
|
+
#
|
8
|
+
# SECURITY IMPLICATIONS:
|
9
|
+
# - Keys remain in memory after use (garbage collection timing is unpredictable)
|
10
|
+
# - Ruby strings cannot be securely wiped from memory
|
11
|
+
# - Memory dumps may contain cryptographic secrets
|
12
|
+
# - Swap files may persist secrets to disk
|
13
|
+
# - String operations create copies that persist in memory
|
14
|
+
#
|
15
|
+
# Ruby provides NO memory safety guarantees for cryptographic secrets.
|
16
|
+
#
|
17
|
+
# For production systems handling sensitive data, consider:
|
18
|
+
# - Hardware Security Modules (HSMs)
|
19
|
+
# - External key management services
|
20
|
+
# - Languages with manual memory management
|
21
|
+
# - Cryptographic appliances with secure memory
|
22
|
+
|
23
|
+
begin
|
24
|
+
require 'rbnacl'
|
25
|
+
rescue LoadError
|
26
|
+
# RbNaCl not available - provider will report as unavailable
|
27
|
+
# To add: gem 'rbnacl', '~> 7.1', '>= 7.1.1'
|
28
|
+
end
|
29
|
+
|
30
|
+
module Familia
|
31
|
+
module Encryption
|
32
|
+
module Providers
|
33
|
+
class XChaCha20Poly1305Provider < Provider
|
34
|
+
ALGORITHM = 'xchacha20poly1305'.freeze
|
35
|
+
NONCE_SIZE = 24
|
36
|
+
AUTH_TAG_SIZE = 16
|
37
|
+
|
38
|
+
def self.available?
|
39
|
+
!!defined?(RbNaCl)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.priority
|
43
|
+
100 # Highest priority - best in class
|
44
|
+
end
|
45
|
+
|
46
|
+
def encrypt(plaintext, key, additional_data = nil)
|
47
|
+
validate_key_length!(key)
|
48
|
+
nonce = generate_nonce
|
49
|
+
box = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(key)
|
50
|
+
|
51
|
+
aad = additional_data.to_s
|
52
|
+
ciphertext_with_tag = box.encrypt(nonce, plaintext.to_s, aad)
|
53
|
+
|
54
|
+
{
|
55
|
+
ciphertext: ciphertext_with_tag[0...-16],
|
56
|
+
auth_tag: ciphertext_with_tag[-16..-1],
|
57
|
+
nonce: nonce
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def decrypt(ciphertext, key, nonce, auth_tag, additional_data = nil)
|
62
|
+
validate_key_length!(key)
|
63
|
+
box = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(key)
|
64
|
+
|
65
|
+
ciphertext_with_tag = ciphertext + auth_tag
|
66
|
+
aad = additional_data.to_s
|
67
|
+
|
68
|
+
box.decrypt(nonce, ciphertext_with_tag, aad)
|
69
|
+
rescue RbNaCl::CryptoError
|
70
|
+
raise EncryptionError, 'Decryption failed - invalid key or corrupted data'
|
71
|
+
end
|
72
|
+
|
73
|
+
def generate_nonce
|
74
|
+
RbNaCl::Random.random_bytes(NONCE_SIZE)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Derives a context-specific encryption key using BLAKE2b.
|
78
|
+
#
|
79
|
+
# The personalization parameter provides cryptographic domain separation,
|
80
|
+
# ensuring that derived keys are unique per application even when using
|
81
|
+
# identical master keys and contexts. This prevents key reuse across
|
82
|
+
# different applications or library versions.
|
83
|
+
#
|
84
|
+
# @param master_key [String] The master key (must be >= 32 bytes)
|
85
|
+
# @param context [String] Context string for key derivation
|
86
|
+
# @param personal [String, nil] Optional personalization override
|
87
|
+
# @return [String] 32-byte derived key
|
88
|
+
def derive_key(master_key, context, personal: nil)
|
89
|
+
validate_key_length!(master_key)
|
90
|
+
raw_personal = personal || Familia.config.encryption_personalization
|
91
|
+
if raw_personal.include?("\0")
|
92
|
+
raise EncryptionError, 'Personalization string must not contain null bytes'
|
93
|
+
end
|
94
|
+
personal_string = raw_personal.ljust(16, "\0")
|
95
|
+
|
96
|
+
RbNaCl::Hash.blake2b(
|
97
|
+
context.force_encoding('BINARY'),
|
98
|
+
key: master_key,
|
99
|
+
digest_size: 32,
|
100
|
+
personal: personal_string
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Clear key from memory (no security guarantees in Ruby)
|
105
|
+
def secure_wipe(key)
|
106
|
+
key&.clear
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.nonce_size
|
110
|
+
NONCE_SIZE
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.auth_tag_size
|
114
|
+
AUTH_TAG_SIZE
|
115
|
+
end
|
116
|
+
|
117
|
+
def nonce_size
|
118
|
+
NONCE_SIZE
|
119
|
+
end
|
120
|
+
|
121
|
+
def auth_tag_size
|
122
|
+
AUTH_TAG_SIZE
|
123
|
+
end
|
124
|
+
|
125
|
+
def algorithm
|
126
|
+
ALGORITHM
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def validate_key_length!(key)
|
132
|
+
raise EncryptionError, 'Key cannot be nil' if key.nil?
|
133
|
+
raise EncryptionError, 'Key must be at least 32 bytes' if key.bytesize < 32
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# lib/familia/encryption/registry.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Encryption
|
5
|
+
# Registry pattern for managing encryption providers
|
6
|
+
class Registry
|
7
|
+
class << self
|
8
|
+
def providers
|
9
|
+
@providers ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def register(provider_class)
|
13
|
+
return unless provider_class.available?
|
14
|
+
|
15
|
+
providers[provider_class::ALGORITHM] = provider_class
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(algorithm)
|
19
|
+
provider_class = providers[algorithm]
|
20
|
+
raise EncryptionError, "Unsupported algorithm: #{algorithm}" unless provider_class
|
21
|
+
|
22
|
+
provider_class.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_provider
|
26
|
+
# Select provider with highest priority
|
27
|
+
@default_provider ||= begin
|
28
|
+
available = providers.values.select(&:available?)
|
29
|
+
available.max_by(&:priority)&.new
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset_default_provider!
|
34
|
+
@default_provider = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def available_algorithms
|
38
|
+
providers.keys
|
39
|
+
end
|
40
|
+
|
41
|
+
# Auto-register known providers
|
42
|
+
def setup!
|
43
|
+
register(Providers::XChaCha20Poly1305Provider)
|
44
|
+
register(Providers::AESGCMProvider)
|
45
|
+
# Future: register(Providers::ChaCha20Poly1305Provider)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# lib/familia/encryption.rb
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'json'
|
5
|
+
require 'openssl'
|
6
|
+
|
7
|
+
# Provider system components
|
8
|
+
require_relative 'encryption/provider'
|
9
|
+
require_relative 'encryption/providers/xchacha20_poly1305_provider'
|
10
|
+
require_relative 'encryption/providers/aes_gcm_provider'
|
11
|
+
require_relative 'encryption/registry'
|
12
|
+
require_relative 'encryption/manager'
|
13
|
+
require_relative 'encryption/encrypted_data'
|
14
|
+
|
15
|
+
module Familia
|
16
|
+
class EncryptionError < StandardError; end
|
17
|
+
|
18
|
+
module Encryption
|
19
|
+
|
20
|
+
# Smart facade with provider selection and field-specific encryption
|
21
|
+
#
|
22
|
+
# Usage in EncryptedFieldType can now be more flexible:
|
23
|
+
#
|
24
|
+
# module Familia
|
25
|
+
# class EncryptedFieldType < FieldType
|
26
|
+
# attr_reader :algorithm # Optional algorithm override
|
27
|
+
#
|
28
|
+
# def initialize(name, aad_fields: [], algorithm: nil, **options)
|
29
|
+
# super(name, **options.merge(on_conflict: :raise))
|
30
|
+
# @aad_fields = Array(aad_fields).freeze
|
31
|
+
# @algorithm = algorithm # Use specific algorithm for this field
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# def encrypt_value(record, value)
|
35
|
+
# context = build_context(record)
|
36
|
+
# additional_data = build_aad(record)
|
37
|
+
#
|
38
|
+
# if @algorithm
|
39
|
+
# # Use specific algorithm for this field
|
40
|
+
# Familia::Encryption.encrypt_with(@algorithm, value,
|
41
|
+
# context: context,
|
42
|
+
# additional_data: additional_data)
|
43
|
+
# else
|
44
|
+
# # Use default best algorithm
|
45
|
+
# Familia::Encryption.encrypt(value,
|
46
|
+
# context: context,
|
47
|
+
# additional_data: additional_data)
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# # Decrypt auto-detects algorithm from data, so no change needed
|
52
|
+
# def decrypt_value(record, encrypted)
|
53
|
+
# context = build_context(record)
|
54
|
+
# additional_data = build_aad(record)
|
55
|
+
#
|
56
|
+
# Familia::Encryption.decrypt(encrypted,
|
57
|
+
# context: context,
|
58
|
+
# additional_data: additional_data)
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
class << self
|
63
|
+
# Get or create a manager with specific algorithm
|
64
|
+
def manager(algorithm: nil)
|
65
|
+
@managers ||= {}
|
66
|
+
@managers[algorithm] ||= Manager.new(algorithm: algorithm)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Quick encryption with auto-selected best provider
|
70
|
+
def encrypt(plaintext, context:, additional_data: nil)
|
71
|
+
manager.encrypt(plaintext, context: context, additional_data: additional_data)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Quick decryption (auto-detects algorithm from data)
|
75
|
+
def decrypt(encrypted_json, context:, additional_data: nil)
|
76
|
+
manager.decrypt(encrypted_json, context: context, additional_data: additional_data)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Encrypt with specific algorithm
|
80
|
+
def encrypt_with(algorithm, plaintext, context:, additional_data: nil)
|
81
|
+
manager(algorithm: algorithm).encrypt(
|
82
|
+
plaintext,
|
83
|
+
context: context,
|
84
|
+
additional_data: additional_data
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Derivation counter for monitoring no-caching behavior
|
89
|
+
def derivation_count
|
90
|
+
@derivation_count ||= Concurrent::AtomicFixnum.new(0)
|
91
|
+
end
|
92
|
+
|
93
|
+
def reset_derivation_count!
|
94
|
+
derivation_count.value = 0
|
95
|
+
end
|
96
|
+
|
97
|
+
# Clear key from memory (no security guarantees in Ruby)
|
98
|
+
def secure_wipe(key)
|
99
|
+
key&.clear
|
100
|
+
end
|
101
|
+
|
102
|
+
# Get info about current encryption setup
|
103
|
+
def status
|
104
|
+
Registry.setup! if Registry.providers.empty?
|
105
|
+
|
106
|
+
{
|
107
|
+
default_algorithm: Registry.default_provider&.algorithm,
|
108
|
+
available_algorithms: Registry.available_algorithms,
|
109
|
+
preferred_available: Registry.default_provider&.class&.name,
|
110
|
+
using_hardware: hardware_acceleration?,
|
111
|
+
key_versions: encryption_keys.keys,
|
112
|
+
current_version: current_key_version
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
# Check if we're using hardware acceleration
|
117
|
+
def hardware_acceleration?
|
118
|
+
provider = Registry.default_provider
|
119
|
+
provider && provider.class.name.include?('Hardware')
|
120
|
+
end
|
121
|
+
|
122
|
+
# Benchmark available providers
|
123
|
+
def benchmark(iterations: 1000)
|
124
|
+
require 'benchmark'
|
125
|
+
test_data = 'x' * 1024 # 1KB test
|
126
|
+
context = 'benchmark:test'
|
127
|
+
|
128
|
+
results = {}
|
129
|
+
Registry.providers.each do |algo, provider_class|
|
130
|
+
next unless provider_class.available?
|
131
|
+
|
132
|
+
mgr = Manager.new(algorithm: algo)
|
133
|
+
time = Benchmark.realtime do
|
134
|
+
iterations.times do
|
135
|
+
encrypted = mgr.encrypt(test_data, context: context)
|
136
|
+
mgr.decrypt(encrypted, context: context)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
results[algo] = {
|
141
|
+
time: time,
|
142
|
+
ops_per_sec: (iterations * 2 / time).round,
|
143
|
+
priority: provider_class.priority
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
results
|
148
|
+
end
|
149
|
+
|
150
|
+
def validate_configuration!
|
151
|
+
raise EncryptionError, 'No encryption keys configured' if encryption_keys.empty?
|
152
|
+
raise EncryptionError, 'No current key version set' unless current_key_version
|
153
|
+
|
154
|
+
current_key = encryption_keys[current_key_version]
|
155
|
+
raise EncryptionError, "Current key version not found: #{current_key_version}" unless current_key
|
156
|
+
|
157
|
+
begin
|
158
|
+
Base64.strict_decode64(current_key)
|
159
|
+
rescue ArgumentError
|
160
|
+
raise EncryptionError, 'Current encryption key is not valid Base64'
|
161
|
+
end
|
162
|
+
|
163
|
+
Registry.setup!
|
164
|
+
raise EncryptionError, 'No encryption providers available' unless Registry.default_provider
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def encryption_keys
|
170
|
+
Familia.config.encryption_keys || {}
|
171
|
+
end
|
172
|
+
|
173
|
+
def current_key_version
|
174
|
+
Familia.config.current_key_version
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|