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
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/types/hashkey.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
class HashKey < DataType
|
@@ -55,12 +55,30 @@ module Familia
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def hgetall
|
58
|
-
dbclient.hgetall(dbkey).each_with_object({}) do |(k,v), ret|
|
58
|
+
dbclient.hgetall(dbkey).each_with_object({}) do |(k, v), ret|
|
59
59
|
ret[k] = deserialize_value v
|
60
60
|
end
|
61
61
|
end
|
62
62
|
alias all hgetall
|
63
63
|
|
64
|
+
# Sets field in the hash stored at key to value, only if field does not yet exist.
|
65
|
+
# If field already exists, this operation has no effect.
|
66
|
+
# @param field [String] The field name
|
67
|
+
# @param val [Object] The value to set
|
68
|
+
# @return [Integer] 1 if field is a new field and value was set, 0 if field already exists
|
69
|
+
def hsetnx(field, val)
|
70
|
+
ret = dbclient.hsetnx dbkey, field.to_s, serialize_value(val)
|
71
|
+
update_expiration if ret == 1
|
72
|
+
ret
|
73
|
+
rescue TypeError => e
|
74
|
+
Familia.le "[hsetnx] #{e.message}"
|
75
|
+
Familia.ld "[hsetnx] #{dbkey} #{field}=#{val}" if Familia.debug
|
76
|
+
echo :hsetnx, caller(1..1).first if Familia.debug # logs via echo to the db and back
|
77
|
+
klass = val.class
|
78
|
+
msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{dbkey}"
|
79
|
+
raise e.class, msg
|
80
|
+
end
|
81
|
+
|
64
82
|
def key?(field)
|
65
83
|
dbclient.hexists dbkey, field.to_s
|
66
84
|
end
|
@@ -1,8 +1,7 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/types/list.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
class List < DataType
|
5
|
-
|
6
5
|
# Returns the number of elements in the list
|
7
6
|
# @return [Integer] number of elements
|
8
7
|
def element_count
|
@@ -91,36 +90,36 @@ module Familia
|
|
91
90
|
rangeraw 0, count
|
92
91
|
end
|
93
92
|
|
94
|
-
def each(&
|
95
|
-
range.each(&
|
93
|
+
def each(&)
|
94
|
+
range.each(&)
|
96
95
|
end
|
97
96
|
|
98
|
-
def each_with_index(&
|
99
|
-
range.each_with_index(&
|
97
|
+
def each_with_index(&)
|
98
|
+
range.each_with_index(&)
|
100
99
|
end
|
101
100
|
|
102
|
-
def eachraw(&
|
103
|
-
rangeraw.each(&
|
101
|
+
def eachraw(&)
|
102
|
+
rangeraw.each(&)
|
104
103
|
end
|
105
104
|
|
106
|
-
def eachraw_with_index(&
|
107
|
-
rangeraw.each_with_index(&
|
105
|
+
def eachraw_with_index(&)
|
106
|
+
rangeraw.each_with_index(&)
|
108
107
|
end
|
109
108
|
|
110
|
-
def collect(&
|
111
|
-
range.collect(&
|
109
|
+
def collect(&)
|
110
|
+
range.collect(&)
|
112
111
|
end
|
113
112
|
|
114
|
-
def select(&
|
115
|
-
range.select(&
|
113
|
+
def select(&)
|
114
|
+
range.select(&)
|
116
115
|
end
|
117
116
|
|
118
|
-
def collectraw(&
|
119
|
-
rangeraw.collect(&
|
117
|
+
def collectraw(&)
|
118
|
+
rangeraw.collect(&)
|
120
119
|
end
|
121
120
|
|
122
|
-
def selectraw(&
|
123
|
-
rangeraw.select(&
|
121
|
+
def selectraw(&)
|
122
|
+
rangeraw.select(&)
|
124
123
|
end
|
125
124
|
|
126
125
|
def at(idx)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# lib/familia/data_type/types/lock.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
class Lock < String
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
@opts[:default] = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
# Acquire a lock with optional TTL
|
11
|
+
# @param token [String] Unique token to identify lock holder (auto-generated if nil)
|
12
|
+
# @param ttl [Integer, nil] Time-to-live in seconds. nil = no expiration, <=0 rejected
|
13
|
+
# @return [String, false] Returns token if acquired successfully, false otherwise
|
14
|
+
def acquire(token = SecureRandom.uuid, ttl: 10)
|
15
|
+
success = setnx(token)
|
16
|
+
# Handle both integer (1/0) and boolean (true/false) return values
|
17
|
+
return false unless success == 1 || success == true
|
18
|
+
return del && false if ttl&.<=(0)
|
19
|
+
return del && false if ttl&.positive? && !expire(ttl)
|
20
|
+
token
|
21
|
+
end
|
22
|
+
|
23
|
+
def release(token)
|
24
|
+
# Lua script to atomically check token and delete
|
25
|
+
script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
|
26
|
+
dbclient.eval(script, [dbkey], [token]) == 1
|
27
|
+
end
|
28
|
+
|
29
|
+
def locked?
|
30
|
+
!value.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def held_by?(token)
|
34
|
+
value == token
|
35
|
+
end
|
36
|
+
|
37
|
+
def force_unlock!
|
38
|
+
del
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Familia::DataType.register Familia::Lock, :lock
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/types/sorted_set.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
class SortedSet < DataType
|
@@ -99,36 +99,36 @@ module Familia
|
|
99
99
|
revrangeraw 0, count, opts
|
100
100
|
end
|
101
101
|
|
102
|
-
def each(&
|
103
|
-
members.each(&
|
102
|
+
def each(&)
|
103
|
+
members.each(&)
|
104
104
|
end
|
105
105
|
|
106
|
-
def each_with_index(&
|
107
|
-
members.each_with_index(&
|
106
|
+
def each_with_index(&)
|
107
|
+
members.each_with_index(&)
|
108
108
|
end
|
109
109
|
|
110
|
-
def collect(&
|
111
|
-
members.collect(&
|
110
|
+
def collect(&)
|
111
|
+
members.collect(&)
|
112
112
|
end
|
113
113
|
|
114
|
-
def select(&
|
115
|
-
members.select(&
|
114
|
+
def select(&)
|
115
|
+
members.select(&)
|
116
116
|
end
|
117
117
|
|
118
|
-
def eachraw(&
|
119
|
-
membersraw.each(&
|
118
|
+
def eachraw(&)
|
119
|
+
membersraw.each(&)
|
120
120
|
end
|
121
121
|
|
122
|
-
def eachraw_with_index(&
|
123
|
-
membersraw.each_with_index(&
|
122
|
+
def eachraw_with_index(&)
|
123
|
+
membersraw.each_with_index(&)
|
124
124
|
end
|
125
125
|
|
126
|
-
def collectraw(&
|
127
|
-
membersraw.collect(&
|
126
|
+
def collectraw(&)
|
127
|
+
membersraw.collect(&)
|
128
128
|
end
|
129
129
|
|
130
|
-
def selectraw(&
|
131
|
-
membersraw.select(&
|
130
|
+
def selectraw(&)
|
131
|
+
membersraw.select(&)
|
132
132
|
end
|
133
133
|
|
134
134
|
def range(sidx, eidx, opts = {})
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/types/string.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
class String < DataType
|
@@ -25,6 +25,7 @@ module Familia
|
|
25
25
|
|
26
26
|
def to_s
|
27
27
|
return super if value.to_s.empty?
|
28
|
+
|
28
29
|
value.to_s
|
29
30
|
end
|
30
31
|
|
@@ -107,12 +108,19 @@ module Familia
|
|
107
108
|
ret
|
108
109
|
end
|
109
110
|
|
111
|
+
def del
|
112
|
+
ret = dbclient.del dbkey
|
113
|
+
ret.positive?
|
114
|
+
end
|
115
|
+
|
110
116
|
def nil?
|
111
117
|
value.nil?
|
112
118
|
end
|
113
119
|
|
114
120
|
Familia::DataType.register self, :string
|
115
|
-
Familia::DataType.register self, :counter
|
116
|
-
Familia::DataType.register self, :lock
|
117
121
|
end
|
118
122
|
end
|
123
|
+
|
124
|
+
# Both subclass String
|
125
|
+
require_relative 'lock'
|
126
|
+
require_relative 'counter'
|
@@ -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
|
@@ -33,7 +32,7 @@ module Familia
|
|
33
32
|
# +methname+ is the term used for the class and instance methods
|
34
33
|
# that are created for the given +klass+ (e.g. set, list, etc)
|
35
34
|
def register(klass, methname)
|
36
|
-
Familia.
|
35
|
+
Familia.trace :REGISTER, nil, "[#{self}] Registering #{klass} as #{methname.inspect}", caller(1..1) if Familia.debug?
|
37
36
|
|
38
37
|
@registered_types[methname] = klass
|
39
38
|
end
|
@@ -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
|
|
@@ -117,7 +115,7 @@ module Familia
|
|
117
115
|
# this point. This would result in a Familia::Problem being raised. So
|
118
116
|
# to be on the safe-side here until we have a better understanding of
|
119
117
|
# the issue, we'll just log the class name for each key-value pair.
|
120
|
-
Familia.
|
118
|
+
Familia.trace :SETTING, nil, " [setting] #{k} #{v.class}", caller(1..1) if Familia.debug?
|
121
119
|
send(:"#{k}=", v) if respond_to? :"#{k}="
|
122
120
|
end
|
123
121
|
|
@@ -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,137 @@
|
|
1
|
+
# lib/familia/encryption/encrypted_data.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Encryption
|
5
|
+
EncryptedData = Data.define(:algorithm, :nonce, :ciphertext, :auth_tag, :key_version) do
|
6
|
+
# Class methods for parsing and validation
|
7
|
+
def self.valid?(json_string)
|
8
|
+
return true if json_string.nil? # Allow nil values
|
9
|
+
return false unless json_string.kind_of?(::String)
|
10
|
+
|
11
|
+
begin
|
12
|
+
parsed = JSON.parse(json_string, symbolize_names: true)
|
13
|
+
return false unless parsed.is_a?(Hash)
|
14
|
+
|
15
|
+
# Check for required fields
|
16
|
+
required_fields = [:algorithm, :nonce, :ciphertext, :auth_tag, :key_version]
|
17
|
+
result = required_fields.all? { |field| parsed.key?(field) }
|
18
|
+
Familia.ld "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}"
|
19
|
+
result
|
20
|
+
rescue JSON::ParserError => e
|
21
|
+
Familia.ld "[valid?] JSON error: #{e.message}"
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.validate!(json_string)
|
27
|
+
return nil if json_string.nil?
|
28
|
+
|
29
|
+
unless json_string.kind_of?(::String)
|
30
|
+
raise EncryptionError, "Expected JSON string, got #{json_string.class}"
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
parsed = JSON.parse(json_string, symbolize_names: true)
|
35
|
+
rescue JSON::ParserError => e
|
36
|
+
raise EncryptionError, "Invalid JSON structure: #{e.message}"
|
37
|
+
end
|
38
|
+
|
39
|
+
unless parsed.is_a?(Hash)
|
40
|
+
raise EncryptionError, "Expected JSON object, got #{parsed.class}"
|
41
|
+
end
|
42
|
+
|
43
|
+
required_fields = [:algorithm, :nonce, :ciphertext, :auth_tag, :key_version]
|
44
|
+
missing_fields = required_fields.reject { |field| parsed.key?(field) }
|
45
|
+
|
46
|
+
unless missing_fields.empty?
|
47
|
+
raise EncryptionError, "Missing required fields: #{missing_fields.join(', ')}"
|
48
|
+
end
|
49
|
+
|
50
|
+
new(**parsed)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.from_json(json_string)
|
54
|
+
validate!(json_string)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Instance methods for decryptability validation
|
58
|
+
def decryptable?
|
59
|
+
return false unless algorithm && nonce && ciphertext && auth_tag && key_version
|
60
|
+
|
61
|
+
# Ensure Registry is set up before checking algorithms
|
62
|
+
Registry.setup! if Registry.providers.empty?
|
63
|
+
|
64
|
+
# Check if algorithm is supported
|
65
|
+
return false unless Registry.providers.key?(algorithm)
|
66
|
+
|
67
|
+
# Validate Base64 encoding of binary fields
|
68
|
+
begin
|
69
|
+
Base64.strict_decode64(nonce)
|
70
|
+
Base64.strict_decode64(ciphertext)
|
71
|
+
Base64.strict_decode64(auth_tag)
|
72
|
+
rescue ArgumentError
|
73
|
+
return false
|
74
|
+
end
|
75
|
+
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_decryptable!
|
80
|
+
unless algorithm
|
81
|
+
raise EncryptionError, "Missing algorithm field"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Ensure Registry is set up before checking algorithms
|
85
|
+
Registry.setup! if Registry.providers.empty?
|
86
|
+
|
87
|
+
unless Registry.providers.key?(algorithm)
|
88
|
+
raise EncryptionError, "Unsupported algorithm: #{algorithm}"
|
89
|
+
end
|
90
|
+
|
91
|
+
unless nonce && ciphertext && auth_tag && key_version
|
92
|
+
missing = []
|
93
|
+
missing << 'nonce' unless nonce
|
94
|
+
missing << 'ciphertext' unless ciphertext
|
95
|
+
missing << 'auth_tag' unless auth_tag
|
96
|
+
missing << 'key_version' unless key_version
|
97
|
+
raise EncryptionError, "Missing required fields: #{missing.join(', ')}"
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get the provider for size validation
|
101
|
+
provider = Registry.providers[algorithm]
|
102
|
+
|
103
|
+
# Validate Base64 encoding and sizes
|
104
|
+
begin
|
105
|
+
decoded_nonce = Base64.strict_decode64(nonce)
|
106
|
+
if decoded_nonce.bytesize != provider.nonce_size
|
107
|
+
raise EncryptionError, "Invalid nonce size: expected #{provider.nonce_size}, got #{decoded_nonce.bytesize}"
|
108
|
+
end
|
109
|
+
rescue ArgumentError
|
110
|
+
raise EncryptionError, "Invalid Base64 encoding in nonce field"
|
111
|
+
end
|
112
|
+
|
113
|
+
begin
|
114
|
+
Base64.strict_decode64(ciphertext) # ciphertext can be variable size
|
115
|
+
rescue ArgumentError
|
116
|
+
raise EncryptionError, "Invalid Base64 encoding in ciphertext field"
|
117
|
+
end
|
118
|
+
|
119
|
+
begin
|
120
|
+
decoded_auth_tag = Base64.strict_decode64(auth_tag)
|
121
|
+
if decoded_auth_tag.bytesize != provider.auth_tag_size
|
122
|
+
raise EncryptionError, "Invalid auth_tag size: expected #{provider.auth_tag_size}, got #{decoded_auth_tag.bytesize}"
|
123
|
+
end
|
124
|
+
rescue ArgumentError
|
125
|
+
raise EncryptionError, "Invalid Base64 encoding in auth_tag field"
|
126
|
+
end
|
127
|
+
|
128
|
+
# Validate that the key version exists
|
129
|
+
unless Familia.config.encryption_keys&.key?(key_version.to_sym)
|
130
|
+
raise EncryptionError, "No key for version: #{key_version}"
|
131
|
+
end
|
132
|
+
|
133
|
+
self
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,119 @@
|
|
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
|
+
# Increment counter immediately to track all decryption attempts, even failed ones
|
37
|
+
Familia::Encryption.derivation_count.increment
|
38
|
+
|
39
|
+
begin
|
40
|
+
data = Familia::Encryption::EncryptedData.new(**JSON.parse(encrypted_json, symbolize_names: true))
|
41
|
+
|
42
|
+
# Validate algorithm support
|
43
|
+
provider = Registry.get(data.algorithm)
|
44
|
+
key = derive_key_without_increment(context, version: data.key_version, provider: provider)
|
45
|
+
|
46
|
+
# Safely decode and validate sizes
|
47
|
+
nonce = decode_and_validate(data.nonce, provider.nonce_size, 'nonce')
|
48
|
+
ciphertext = decode_and_validate_ciphertext(data.ciphertext)
|
49
|
+
auth_tag = decode_and_validate(data.auth_tag, provider.auth_tag_size, 'auth_tag')
|
50
|
+
|
51
|
+
provider.decrypt(ciphertext, key, nonce, auth_tag, additional_data)
|
52
|
+
rescue EncryptionError
|
53
|
+
raise
|
54
|
+
rescue JSON::ParserError => e
|
55
|
+
raise EncryptionError, "Invalid JSON structure: #{e.message}"
|
56
|
+
rescue StandardError => e
|
57
|
+
raise EncryptionError, "Decryption failed: #{e.message}"
|
58
|
+
end
|
59
|
+
ensure
|
60
|
+
Familia::Encryption.secure_wipe(key) if key
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def decode_and_validate(encoded, expected_size, component)
|
66
|
+
decoded = Base64.strict_decode64(encoded)
|
67
|
+
raise EncryptionError, 'Invalid encrypted data' unless decoded.bytesize == expected_size
|
68
|
+
decoded
|
69
|
+
rescue ArgumentError => e
|
70
|
+
raise EncryptionError, "Invalid Base64 encoding in #{component} field"
|
71
|
+
end
|
72
|
+
|
73
|
+
def decode_and_validate_ciphertext(encoded)
|
74
|
+
Base64.strict_decode64(encoded)
|
75
|
+
rescue ArgumentError => e
|
76
|
+
raise EncryptionError, "Invalid Base64 encoding in ciphertext field"
|
77
|
+
end
|
78
|
+
|
79
|
+
def derive_key(context, version: nil, provider: nil)
|
80
|
+
# Increment counter to prove no caching is happening
|
81
|
+
Familia::Encryption.derivation_count.increment
|
82
|
+
|
83
|
+
derive_key_without_increment(context, version: version, provider: provider)
|
84
|
+
end
|
85
|
+
|
86
|
+
def derive_key_without_increment(context, version: nil, provider: nil)
|
87
|
+
# Use provided provider or fall back to instance provider
|
88
|
+
provider ||= @provider
|
89
|
+
|
90
|
+
# Require explicit provider in decrypt context
|
91
|
+
raise EncryptionError, 'Provider required for key derivation' unless provider
|
92
|
+
|
93
|
+
version ||= current_key_version
|
94
|
+
master_key = get_master_key(version)
|
95
|
+
|
96
|
+
provider.derive_key(master_key, context)
|
97
|
+
ensure
|
98
|
+
Familia::Encryption.secure_wipe(master_key) if master_key
|
99
|
+
end
|
100
|
+
|
101
|
+
def get_master_key(version)
|
102
|
+
raise EncryptionError, 'Key version cannot be nil' if version.nil?
|
103
|
+
|
104
|
+
key = encryption_keys[version] || encryption_keys[version.to_sym] || encryption_keys[version.to_s]
|
105
|
+
raise EncryptionError, "No key for version: #{version}" unless key
|
106
|
+
|
107
|
+
Base64.strict_decode64(key)
|
108
|
+
end
|
109
|
+
|
110
|
+
def encryption_keys
|
111
|
+
Familia.config.encryption_keys || {}
|
112
|
+
end
|
113
|
+
|
114
|
+
def current_key_version
|
115
|
+
Familia.config.current_key_version
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
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
|