familia 2.0.0.pre3 → 2.0.0.pre5

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +3 -3
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +18 -3
  7. data/README.md +36 -157
  8. data/TEST_COVERAGE.md +40 -0
  9. data/docs/overview.md +359 -0
  10. data/docs/wiki/API-Reference.md +270 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
  12. data/docs/wiki/Home.md +49 -0
  13. data/docs/wiki/Implementation-Guide.md +183 -0
  14. data/docs/wiki/Security-Model.md +143 -0
  15. data/lib/familia/base.rb +18 -27
  16. data/lib/familia/connection.rb +6 -5
  17. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  18. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  19. data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
  20. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  21. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  22. data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
  23. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  24. data/lib/familia/{datatype.rb → data_type.rb} +10 -12
  25. data/lib/familia/encryption/manager.rb +102 -0
  26. data/lib/familia/encryption/provider.rb +49 -0
  27. data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
  28. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  29. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
  30. data/lib/familia/encryption/registry.rb +50 -0
  31. data/lib/familia/encryption.rb +178 -0
  32. data/lib/familia/encryption_request_cache.rb +68 -0
  33. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
  34. data/lib/familia/features/encrypted_fields.rb +28 -0
  35. data/lib/familia/features/expiration.rb +107 -77
  36. data/lib/familia/features/quantization.rb +5 -9
  37. data/lib/familia/features/relatable_objects.rb +2 -4
  38. data/lib/familia/features/safe_dump.rb +14 -17
  39. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  40. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  41. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  42. data/lib/familia/features/transient_fields.rb +47 -0
  43. data/lib/familia/features.rb +40 -24
  44. data/lib/familia/field_type.rb +270 -0
  45. data/lib/familia/horreum/connection.rb +8 -11
  46. data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
  47. data/lib/familia/horreum/definition_methods.rb +453 -0
  48. data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
  49. data/lib/familia/horreum/serialization.rb +46 -18
  50. data/lib/familia/horreum/settings.rb +10 -2
  51. data/lib/familia/horreum/utils.rb +9 -10
  52. data/lib/familia/horreum.rb +18 -10
  53. data/lib/familia/logging.rb +14 -14
  54. data/lib/familia/settings.rb +39 -3
  55. data/lib/familia/utils.rb +45 -0
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -1
  58. data/try/core/base_enhancements_try.rb +115 -0
  59. data/try/core/connection_try.rb +0 -1
  60. data/try/core/errors_try.rb +0 -1
  61. data/try/core/familia_extended_try.rb +3 -4
  62. data/try/core/familia_try.rb +0 -1
  63. data/try/core/pools_try.rb +2 -2
  64. data/try/core/secure_identifier_try.rb +0 -1
  65. data/try/core/settings_try.rb +0 -1
  66. data/try/core/utils_try.rb +0 -1
  67. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  68. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  69. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  70. data/try/{datatypes → data_types}/list_try.rb +1 -2
  71. data/try/{datatypes → data_types}/set_try.rb +1 -2
  72. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  73. data/try/{datatypes → data_types}/string_try.rb +1 -2
  74. data/try/debugging/README.md +32 -0
  75. data/try/debugging/cache_behavior_tracer.rb +91 -0
  76. data/try/debugging/encryption_method_tracer.rb +138 -0
  77. data/try/debugging/provider_diagnostics.rb +110 -0
  78. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  79. data/try/edge_cases/json_serialization_try.rb +0 -1
  80. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  81. data/try/encryption/config_persistence_try.rb +192 -0
  82. data/try/encryption/encryption_core_try.rb +328 -0
  83. data/try/encryption/instance_variable_scope_try.rb +31 -0
  84. data/try/encryption/module_loading_try.rb +28 -0
  85. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  86. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  87. data/try/encryption/roundtrip_validation_try.rb +28 -0
  88. data/try/encryption/secure_memory_handling_try.rb +125 -0
  89. data/try/features/encrypted_fields_core_try.rb +117 -0
  90. data/try/features/encrypted_fields_integration_try.rb +220 -0
  91. data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
  92. data/try/features/encrypted_fields_security_try.rb +370 -0
  93. data/try/features/encryption_fields/aad_protection_try.rb +53 -0
  94. data/try/features/encryption_fields/context_isolation_try.rb +120 -0
  95. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  96. data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
  97. data/try/features/encryption_fields/fresh_key_try.rb +163 -0
  98. data/try/features/encryption_fields/key_rotation_try.rb +117 -0
  99. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  100. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  101. data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
  102. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  103. data/try/features/expiration_try.rb +0 -1
  104. data/try/features/feature_dependencies_try.rb +159 -0
  105. data/try/features/quantization_try.rb +0 -1
  106. data/try/features/real_feature_integration_try.rb +148 -0
  107. data/try/features/relatable_objects_try.rb +0 -1
  108. data/try/features/safe_dump_advanced_try.rb +0 -1
  109. data/try/features/safe_dump_try.rb +0 -1
  110. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  111. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  112. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  113. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  114. data/try/features/transient_fields_core_try.rb +181 -0
  115. data/try/features/transient_fields_integration_try.rb +260 -0
  116. data/try/helpers/test_helpers.rb +42 -0
  117. data/try/horreum/base_try.rb +157 -3
  118. data/try/horreum/class_methods_try.rb +27 -36
  119. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  120. data/try/horreum/field_categories_try.rb +118 -0
  121. data/try/horreum/field_definition_try.rb +96 -0
  122. data/try/horreum/initialization_try.rb +0 -1
  123. data/try/horreum/relations_try.rb +0 -1
  124. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  125. data/try/horreum/serialization_try.rb +2 -3
  126. data/try/memory/memory_basic_test.rb +73 -0
  127. data/try/memory/memory_detailed_test.rb +121 -0
  128. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  129. data/try/memory/memory_search_for_string.rb +83 -0
  130. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  131. data/try/models/customer_safe_dump_try.rb +0 -1
  132. data/try/models/customer_try.rb +0 -1
  133. data/try/models/datatype_base_try.rb +1 -2
  134. data/try/models/familia_object_try.rb +0 -1
  135. metadata +85 -18
@@ -1,8 +1,7 @@
1
- # lib/familia/datatype/types/unsorted_set.rb
1
+ # lib/familia/data_type/types/unsorted_set.rb
2
2
 
3
3
  module Familia
4
4
  class Set < DataType
5
-
6
5
  # Returns the number of elements in the unsorted set
7
6
  # @return [Integer] number of elements
8
7
  def element_count
@@ -36,36 +35,36 @@ module Familia
36
35
  dbclient.smembers(dbkey)
37
36
  end
38
37
 
39
- def each(&blk)
40
- members.each(&blk)
38
+ def each(&)
39
+ members.each(&)
41
40
  end
42
41
 
43
- def each_with_index(&blk)
44
- members.each_with_index(&blk)
42
+ def each_with_index(&)
43
+ members.each_with_index(&)
45
44
  end
46
45
 
47
- def collect(&blk)
48
- members.collect(&blk)
46
+ def collect(&)
47
+ members.collect(&)
49
48
  end
50
49
 
51
- def select(&blk)
52
- members.select(&blk)
50
+ def select(&)
51
+ members.select(&)
53
52
  end
54
53
 
55
- def eachraw(&blk)
56
- membersraw.each(&blk)
54
+ def eachraw(&)
55
+ membersraw.each(&)
57
56
  end
58
57
 
59
- def eachraw_with_index(&blk)
60
- membersraw.each_with_index(&blk)
58
+ def eachraw_with_index(&)
59
+ membersraw.each_with_index(&)
61
60
  end
62
61
 
63
- def collectraw(&blk)
64
- membersraw.collect(&blk)
62
+ def collectraw(&)
63
+ membersraw.collect(&)
65
64
  end
66
65
 
67
- def selectraw(&blk)
68
- membersraw.select(&blk)
66
+ def selectraw(&)
67
+ membersraw.select(&)
69
68
  end
70
69
 
71
70
  def member?(val)
@@ -1,10 +1,9 @@
1
- # lib/familia/datatype.rb
1
+ # lib/familia/data_type.rb
2
2
 
3
- require_relative 'datatype/commands'
4
- require_relative 'datatype/serialization'
3
+ require_relative 'data_type/commands'
4
+ require_relative 'data_type/serialization'
5
5
 
6
6
  module Familia
7
-
8
7
  # DataType - Base class for Database data type wrappers
9
8
  #
10
9
  # This class provides common functionality for various Database data types
@@ -54,7 +53,7 @@ module Familia
54
53
  obj.default_expiration = default_expiration # method added via Features::Expiration
55
54
  obj.uri = uri
56
55
  obj.parent = self
57
- super(obj)
56
+ super
58
57
  end
59
58
 
60
59
  def valid_keys_only(opts)
@@ -102,7 +101,6 @@ module Familia
102
101
  # Connection precendence: uses the database connection of the parent or the
103
102
  # value of opts[:dbclient] or Familia.dbclient (in that order).
104
103
  def initialize(keystring, opts = {})
105
- #Familia.ld " [initializing] #{self.class} #{opts}"
106
104
  @keystring = keystring
107
105
  @keystring = @keystring.join(Familia.delim) if @keystring.is_a?(Array)
108
106
 
@@ -172,7 +170,7 @@ module Familia
172
170
  parent.dbkey(keystring)
173
171
  elsif parent_class?
174
172
  # This is a class-level datatype object so the parent class' dbkey
175
- # method is defined in Familia::Horreum::ClassMethods.
173
+ # method is defined in Familia::Horreum::DefinitionMethods.
176
174
  parent.dbkey(keystring, nil)
177
175
  else
178
176
  # This is a standalone DataType object where it's keystring
@@ -235,9 +233,9 @@ module Familia
235
233
  include Serialization
236
234
  end
237
235
 
238
- require_relative 'datatype/types/list'
239
- require_relative 'datatype/types/unsorted_set'
240
- require_relative 'datatype/types/sorted_set'
241
- require_relative 'datatype/types/hashkey'
242
- require_relative 'datatype/types/string'
236
+ require_relative 'data_type/types/list'
237
+ require_relative 'data_type/types/unsorted_set'
238
+ require_relative 'data_type/types/sorted_set'
239
+ require_relative 'data_type/types/hashkey'
240
+ require_relative 'data_type/types/string'
243
241
  end
@@ -0,0 +1,102 @@
1
+ # lib/familia/encryption/manager.rb
2
+
3
+ module Familia
4
+ module Encryption
5
+ # High-level encryption manager - replaces monolithic Encryption module
6
+ class Manager
7
+ attr_reader :provider
8
+
9
+ def initialize(algorithm: nil)
10
+ Registry.setup! if Registry.providers.empty?
11
+ @provider = algorithm ? Registry.get(algorithm) : Registry.default_provider
12
+ raise EncryptionError, 'No encryption provider available' unless @provider
13
+ end
14
+
15
+ def encrypt(plaintext, context:, additional_data: nil)
16
+ return nil if plaintext.to_s.empty?
17
+
18
+ key = derive_key(context)
19
+
20
+ result = @provider.encrypt(plaintext, key, additional_data)
21
+
22
+ Familia::Encryption::EncryptedData.new(
23
+ algorithm: @provider.algorithm,
24
+ nonce: Base64.strict_encode64(result[:nonce]),
25
+ ciphertext: Base64.strict_encode64(result[:ciphertext]),
26
+ auth_tag: Base64.strict_encode64(result[:auth_tag]),
27
+ key_version: current_key_version
28
+ ).to_h.to_json
29
+ ensure
30
+ Familia::Encryption.secure_wipe(key) if key
31
+ end
32
+
33
+ def decrypt(encrypted_json, context:, additional_data: nil)
34
+ return nil if encrypted_json.nil? || encrypted_json.empty?
35
+
36
+ begin
37
+ data = Familia::Encryption::EncryptedData.new(**JSON.parse(encrypted_json, symbolize_names: true))
38
+
39
+ # Validate algorithm support
40
+ provider = Registry.get(data.algorithm)
41
+ key = derive_key(context, version: data.key_version, provider: provider)
42
+
43
+ # Safely decode and validate sizes
44
+ nonce = decode_and_validate(data.nonce, provider.nonce_size, 'nonce')
45
+ ciphertext = Base64.strict_decode64(data.ciphertext)
46
+ auth_tag = decode_and_validate(data.auth_tag, provider.auth_tag_size, 'auth_tag')
47
+
48
+ provider.decrypt(ciphertext, key, nonce, auth_tag, additional_data)
49
+ rescue EncryptionError
50
+ raise
51
+ rescue StandardError
52
+ raise EncryptionError, 'Decryption failed'
53
+ end
54
+ ensure
55
+ Familia::Encryption.secure_wipe(key) if key
56
+ end
57
+
58
+ private
59
+
60
+ def decode_and_validate(encoded, expected_size, component)
61
+ decoded = Base64.strict_decode64(encoded)
62
+ raise EncryptionError, 'Invalid encrypted data' unless decoded.bytesize == expected_size
63
+ decoded
64
+ end
65
+
66
+ def derive_key(context, version: nil, provider: nil)
67
+ # Increment counter to prove no caching is happening
68
+ Familia::Encryption.derivation_count.increment
69
+
70
+ # Use provided provider or fall back to instance provider
71
+ provider ||= @provider
72
+
73
+ # Require explicit provider in decrypt context
74
+ raise EncryptionError, 'Provider required for key derivation' unless provider
75
+
76
+ version ||= current_key_version
77
+ master_key = get_master_key(version)
78
+
79
+ provider.derive_key(master_key, context)
80
+ ensure
81
+ Familia::Encryption.secure_wipe(master_key) if master_key
82
+ end
83
+
84
+ def get_master_key(version)
85
+ raise EncryptionError, 'Key version cannot be nil' if version.nil?
86
+
87
+ key = encryption_keys[version] || encryption_keys[version.to_sym] || encryption_keys[version.to_s]
88
+ raise EncryptionError, "No key for version: #{version}" unless key
89
+
90
+ Base64.strict_decode64(key)
91
+ end
92
+
93
+ def encryption_keys
94
+ Familia.config.encryption_keys || {}
95
+ end
96
+
97
+ def current_key_version
98
+ Familia.config.current_key_version
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,49 @@
1
+ # lib/familia/encryption/provider.rb
2
+
3
+ module Familia
4
+ module Encryption
5
+ # Base provider class - similar to FieldType pattern
6
+ class Provider
7
+ attr_reader :algorithm, :nonce_size, :auth_tag_size
8
+
9
+ def initialize
10
+ @algorithm = self.class::ALGORITHM
11
+ @nonce_size = self.class::NONCE_SIZE
12
+ @auth_tag_size = self.class::AUTH_TAG_SIZE
13
+ end
14
+
15
+ # Public interface methods that subclasses must implement
16
+ def encrypt(plaintext, key, additional_data = nil)
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def decrypt(ciphertext, key, nonce, auth_tag, additional_data = nil)
21
+ raise NotImplementedError
22
+ end
23
+
24
+ def generate_nonce
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def derive_key(master_key, context)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ # Clear key from memory (best effort, no security guarantees)
33
+ # Ruby provides no reliable way to securely wipe memory
34
+ def secure_wipe(key)
35
+ key&.clear if key.respond_to?(:clear)
36
+ end
37
+
38
+ # Check if this provider is available
39
+ def self.available?
40
+ raise NotImplementedError
41
+ end
42
+
43
+ # Priority for automatic selection (higher = preferred)
44
+ def self.priority
45
+ 0
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,103 @@
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
+ private
91
+
92
+ def create_cipher(mode)
93
+ OpenSSL::Cipher.new('aes-256-gcm').tap { |c| c.public_send(mode) }
94
+ end
95
+
96
+ def validate_key_length!(key)
97
+ raise EncryptionError, 'Key cannot be nil' if key.nil?
98
+ raise EncryptionError, 'Key must be at least 32 bytes' if key.bytesize < 32
99
+ end
100
+ end
101
+ end
102
+ end
103
+ 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,118 @@
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
+ private
110
+
111
+ def validate_key_length!(key)
112
+ raise EncryptionError, 'Key cannot be nil' if key.nil?
113
+ raise EncryptionError, 'Key must be at least 32 bytes' if key.bytesize < 32
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end