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.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +11 -8
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +19 -3
  7. data/README.md +36 -157
  8. data/docs/overview.md +359 -0
  9. data/docs/wiki/API-Reference.md +347 -0
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +600 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +106 -0
  17. data/docs/wiki/Implementation-Guide.md +276 -0
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  20. data/docs/wiki/Security-Model.md +183 -0
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/lib/familia/base.rb +18 -27
  23. data/lib/familia/connection.rb +6 -5
  24. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  25. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  26. data/lib/familia/data_type/types/counter.rb +38 -0
  27. data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
  28. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  31. data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
  32. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  33. data/lib/familia/{datatype.rb → data_type.rb} +12 -14
  34. data/lib/familia/encryption/encrypted_data.rb +137 -0
  35. data/lib/familia/encryption/manager.rb +119 -0
  36. data/lib/familia/encryption/provider.rb +49 -0
  37. data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
  38. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  39. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
  40. data/lib/familia/encryption/registry.rb +50 -0
  41. data/lib/familia/encryption.rb +178 -0
  42. data/lib/familia/encryption_request_cache.rb +68 -0
  43. data/lib/familia/errors.rb +17 -3
  44. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  45. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
  46. data/lib/familia/features/encrypted_fields.rb +28 -0
  47. data/lib/familia/features/expiration.rb +107 -77
  48. data/lib/familia/features/quantization.rb +5 -9
  49. data/lib/familia/features/relatable_objects.rb +2 -4
  50. data/lib/familia/features/safe_dump.rb +14 -17
  51. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  52. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  53. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  54. data/lib/familia/features/transient_fields.rb +47 -0
  55. data/lib/familia/features.rb +40 -24
  56. data/lib/familia/field_type.rb +273 -0
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
  58. data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
  63. data/lib/familia/horreum/subclass/definition.rb +469 -0
  64. data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +30 -22
  67. data/lib/familia/logging.rb +14 -14
  68. data/lib/familia/settings.rb +39 -3
  69. data/lib/familia/utils.rb +45 -0
  70. data/lib/familia/version.rb +1 -1
  71. data/lib/familia.rb +3 -2
  72. data/try/core/base_enhancements_try.rb +115 -0
  73. data/try/core/connection_try.rb +0 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -5
  77. data/try/core/familia_extended_try.rb +3 -4
  78. data/try/core/familia_try.rb +1 -2
  79. data/try/core/persistence_operations_try.rb +297 -0
  80. data/try/core/pools_try.rb +2 -2
  81. data/try/core/secure_identifier_try.rb +0 -1
  82. data/try/core/settings_try.rb +0 -1
  83. data/try/core/utils_try.rb +0 -1
  84. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  85. data/try/data_types/counter_try.rb +93 -0
  86. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  87. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  88. data/try/{datatypes → data_types}/list_try.rb +1 -2
  89. data/try/data_types/lock_try.rb +133 -0
  90. data/try/{datatypes → data_types}/set_try.rb +1 -2
  91. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  92. data/try/{datatypes → data_types}/string_try.rb +1 -2
  93. data/try/debugging/README.md +32 -0
  94. data/try/debugging/cache_behavior_tracer.rb +91 -0
  95. data/try/debugging/debug_aad_process.rb +82 -0
  96. data/try/debugging/debug_concealed_internal.rb +59 -0
  97. data/try/debugging/debug_concealed_reveal.rb +61 -0
  98. data/try/debugging/debug_context_aad.rb +68 -0
  99. data/try/debugging/debug_context_simple.rb +80 -0
  100. data/try/debugging/debug_cross_context.rb +62 -0
  101. data/try/debugging/debug_database_load.rb +64 -0
  102. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  103. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  104. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  105. data/try/debugging/debug_field_decrypt.rb +74 -0
  106. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  107. data/try/debugging/debug_load_path.rb +66 -0
  108. data/try/debugging/debug_method_definition.rb +46 -0
  109. data/try/debugging/debug_method_resolution.rb +41 -0
  110. data/try/debugging/debug_minimal.rb +24 -0
  111. data/try/debugging/debug_provider.rb +68 -0
  112. data/try/debugging/debug_secure_behavior.rb +73 -0
  113. data/try/debugging/debug_string_class.rb +46 -0
  114. data/try/debugging/debug_test.rb +46 -0
  115. data/try/debugging/debug_test_design.rb +80 -0
  116. data/try/debugging/encryption_method_tracer.rb +138 -0
  117. data/try/debugging/provider_diagnostics.rb +110 -0
  118. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  119. data/try/edge_cases/json_serialization_try.rb +0 -1
  120. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  121. data/try/encryption/config_persistence_try.rb +192 -0
  122. data/try/encryption/encryption_core_try.rb +328 -0
  123. data/try/encryption/instance_variable_scope_try.rb +31 -0
  124. data/try/encryption/module_loading_try.rb +28 -0
  125. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  126. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  127. data/try/encryption/roundtrip_validation_try.rb +28 -0
  128. data/try/encryption/secure_memory_handling_try.rb +125 -0
  129. data/try/features/encrypted_fields_core_try.rb +125 -0
  130. data/try/features/encrypted_fields_integration_try.rb +216 -0
  131. data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
  132. data/try/features/encrypted_fields_security_try.rb +377 -0
  133. data/try/features/encryption_fields/aad_protection_try.rb +138 -0
  134. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  135. data/try/features/encryption_fields/context_isolation_try.rb +141 -0
  136. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  137. data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
  138. data/try/features/encryption_fields/fresh_key_try.rb +168 -0
  139. data/try/features/encryption_fields/key_rotation_try.rb +123 -0
  140. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  141. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  142. data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
  143. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  144. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  145. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  146. data/try/features/expiration_try.rb +0 -1
  147. data/try/features/feature_dependencies_try.rb +159 -0
  148. data/try/features/quantization_try.rb +0 -1
  149. data/try/features/real_feature_integration_try.rb +148 -0
  150. data/try/features/relatable_objects_try.rb +0 -1
  151. data/try/features/safe_dump_advanced_try.rb +0 -1
  152. data/try/features/safe_dump_try.rb +0 -1
  153. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  154. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  155. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  156. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  157. data/try/features/transient_fields_core_try.rb +181 -0
  158. data/try/features/transient_fields_integration_try.rb +260 -0
  159. data/try/helpers/test_helpers.rb +67 -0
  160. data/try/horreum/base_try.rb +157 -3
  161. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  162. data/try/horreum/field_categories_try.rb +118 -0
  163. data/try/horreum/field_definition_try.rb +96 -0
  164. data/try/horreum/initialization_try.rb +1 -2
  165. data/try/horreum/relations_try.rb +1 -2
  166. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  167. data/try/horreum/serialization_try.rb +41 -7
  168. data/try/memory/memory_basic_test.rb +73 -0
  169. data/try/memory/memory_detailed_test.rb +121 -0
  170. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  171. data/try/memory/memory_search_for_string.rb +83 -0
  172. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  173. data/try/models/customer_safe_dump_try.rb +1 -2
  174. data/try/models/customer_try.rb +1 -2
  175. data/try/models/datatype_base_try.rb +1 -2
  176. data/try/models/familia_object_try.rb +0 -1
  177. metadata +131 -23
  178. 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