familia 2.0.0.pre4 → 2.0.0.pre6

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