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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +3 -3
- data/Gemfile +5 -1
- data/Gemfile.lock +18 -3
- data/README.md +36 -157
- data/TEST_COVERAGE.md +40 -0
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +270 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
- data/docs/wiki/Home.md +49 -0
- data/docs/wiki/Implementation-Guide.md +183 -0
- data/docs/wiki/Security-Model.md +143 -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/{datatype → data_type}/types/hashkey.rb +2 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +10 -12
- data/lib/familia/encryption/manager.rb +102 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -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/features/encrypted_fields/encrypted_field_type.rb +153 -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 +270 -0
- data/lib/familia/horreum/connection.rb +8 -11
- data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
- data/lib/familia/horreum/definition_methods.rb +453 -0
- data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
- data/lib/familia/horreum/serialization.rb +46 -18
- data/lib/familia/horreum/settings.rb +10 -2
- data/lib/familia/horreum/utils.rb +9 -10
- data/lib/familia/horreum.rb +18 -10
- 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 +2 -1
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/errors_try.rb +0 -1
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +0 -1
- 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/{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/{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/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 +117 -0
- data/try/features/encrypted_fields_integration_try.rb +220 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
- data/try/features/encrypted_fields_security_try.rb +370 -0
- data/try/features/encryption_fields/aad_protection_try.rb +53 -0
- data/try/features/encryption_fields/context_isolation_try.rb +120 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
- data/try/features/encryption_fields/fresh_key_try.rb +163 -0
- data/try/features/encryption_fields/key_rotation_try.rb +117 -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 +54 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -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 +42 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/class_methods_try.rb +27 -36
- 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 +0 -1
- data/try/horreum/relations_try.rb +0 -1
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +2 -3
- 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 +0 -1
- data/try/models/customer_try.rb +0 -1
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +85 -18
@@ -1,8 +1,7 @@
|
|
1
|
-
# lib/familia/
|
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(&
|
40
|
-
members.each(&
|
38
|
+
def each(&)
|
39
|
+
members.each(&)
|
41
40
|
end
|
42
41
|
|
43
|
-
def each_with_index(&
|
44
|
-
members.each_with_index(&
|
42
|
+
def each_with_index(&)
|
43
|
+
members.each_with_index(&)
|
45
44
|
end
|
46
45
|
|
47
|
-
def collect(&
|
48
|
-
members.collect(&
|
46
|
+
def collect(&)
|
47
|
+
members.collect(&)
|
49
48
|
end
|
50
49
|
|
51
|
-
def select(&
|
52
|
-
members.select(&
|
50
|
+
def select(&)
|
51
|
+
members.select(&)
|
53
52
|
end
|
54
53
|
|
55
|
-
def eachraw(&
|
56
|
-
membersraw.each(&
|
54
|
+
def eachraw(&)
|
55
|
+
membersraw.each(&)
|
57
56
|
end
|
58
57
|
|
59
|
-
def eachraw_with_index(&
|
60
|
-
membersraw.each_with_index(&
|
58
|
+
def eachraw_with_index(&)
|
59
|
+
membersraw.each_with_index(&)
|
61
60
|
end
|
62
61
|
|
63
|
-
def collectraw(&
|
64
|
-
membersraw.collect(&
|
62
|
+
def collectraw(&)
|
63
|
+
membersraw.collect(&)
|
65
64
|
end
|
66
65
|
|
67
|
-
def selectraw(&
|
68
|
-
membersraw.select(&
|
66
|
+
def selectraw(&)
|
67
|
+
membersraw.select(&)
|
69
68
|
end
|
70
69
|
|
71
70
|
def member?(val)
|
@@ -1,10 +1,9 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type.rb
|
2
2
|
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
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
|
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::
|
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 '
|
239
|
-
require_relative '
|
240
|
-
require_relative '
|
241
|
-
require_relative '
|
242
|
-
require_relative '
|
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
|