familia 2.0.0.pre4 → 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 -243
- 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/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,11 @@
|
|
1
1
|
# lib/familia/features/quantization.rb
|
2
2
|
|
3
3
|
module Familia::Features
|
4
|
-
|
5
4
|
module Quantization
|
5
|
+
def self.included(base)
|
6
|
+
Familia.ld "[#{base}] Loaded #{self}"
|
7
|
+
base.extend ClassMethods
|
8
|
+
end
|
6
9
|
|
7
10
|
module ClassMethods
|
8
11
|
# Generates a quantized timestamp based on the given parameters.
|
@@ -25,9 +28,7 @@ module Familia::Features
|
|
25
28
|
#
|
26
29
|
def qstamp(quantum = nil, pattern: nil, time: nil)
|
27
30
|
# Handle default values and array input
|
28
|
-
if quantum.is_a?(Array)
|
29
|
-
quantum, pattern = quantum
|
30
|
-
end
|
31
|
+
quantum, pattern = quantum if quantum.is_a?(Array)
|
31
32
|
|
32
33
|
# Previously we erronously included `@opts.fetch(:quantize, nil)` in
|
33
34
|
# the list of default values here, but @opts is for horreum instances
|
@@ -46,11 +47,6 @@ module Familia::Features
|
|
46
47
|
end
|
47
48
|
end
|
48
49
|
|
49
|
-
def self.included base
|
50
|
-
Familia.ld "[#{base}] Loaded #{self}"
|
51
|
-
base.extend ClassMethods
|
52
|
-
end
|
53
|
-
|
54
50
|
def qstamp(quantum = nil, pattern: nil, time: nil)
|
55
51
|
self.class.qstamp(quantum || self.class.default_expiration, pattern: pattern, time: time)
|
56
52
|
end
|
@@ -9,9 +9,6 @@ module V2
|
|
9
9
|
# Provides the standard core object fields and methods.
|
10
10
|
#
|
11
11
|
module RelatableObject
|
12
|
-
klass = self
|
13
|
-
err_klass = V2::Features::RelatableObjectError
|
14
|
-
|
15
12
|
def self.included(base)
|
16
13
|
base.class_sorted_set :relatable_objids
|
17
14
|
base.class_hashkey :owners
|
@@ -82,7 +79,8 @@ module V2
|
|
82
79
|
|
83
80
|
def owned?
|
84
81
|
# We can only have an owner if we are relatable ourselves.
|
85
|
-
return false unless
|
82
|
+
return false unless is_a?(RelatableObject)
|
83
|
+
|
86
84
|
# If our object identifier is present, we have an owner
|
87
85
|
self.class.owners.key?(objid)
|
88
86
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# lib/familia/features/safe_dump.rb
|
2
2
|
|
3
|
-
|
4
3
|
module Familia::Features
|
5
4
|
# SafeDump is a mixin that allows models to define a list of fields that are
|
6
5
|
# safe to dump. This is useful for serializing objects to JSON or other
|
@@ -54,6 +53,20 @@ module Familia::Features
|
|
54
53
|
@safe_dump_fields = []
|
55
54
|
@safe_dump_field_map = {}
|
56
55
|
|
56
|
+
def self.included(base)
|
57
|
+
Familia.ld "[#{self}] Enabled in #{base}"
|
58
|
+
base.extend ClassMethods
|
59
|
+
|
60
|
+
# Optionally define safe_dump_fields in the class to make
|
61
|
+
# sure we always have an array to work with.
|
62
|
+
base.instance_variable_set(:@safe_dump_fields, []) unless base.instance_variable_defined?(:@safe_dump_fields)
|
63
|
+
|
64
|
+
# Ditto for the field map
|
65
|
+
return if base.instance_variable_defined?(:@safe_dump_field_map)
|
66
|
+
|
67
|
+
base.instance_variable_set(:@safe_dump_field_map, {})
|
68
|
+
end
|
69
|
+
|
57
70
|
module ClassMethods
|
58
71
|
def set_safe_dump_fields(*fields)
|
59
72
|
@safe_dump_fields = fields
|
@@ -100,22 +113,6 @@ module Familia::Features
|
|
100
113
|
end
|
101
114
|
end
|
102
115
|
|
103
|
-
def self.included base
|
104
|
-
Familia.ld "[#{self}] Enabled in #{base}"
|
105
|
-
base.extend ClassMethods
|
106
|
-
|
107
|
-
# Optionally define safe_dump_fields in the class to make
|
108
|
-
# sure we always have an array to work with.
|
109
|
-
unless base.instance_variable_defined?(:@safe_dump_fields)
|
110
|
-
base.instance_variable_set(:@safe_dump_fields, [])
|
111
|
-
end
|
112
|
-
|
113
|
-
# Ditto for the field map
|
114
|
-
unless base.instance_variable_defined?(:@safe_dump_field_map)
|
115
|
-
base.instance_variable_set(:@safe_dump_field_map, {})
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
116
|
# Returns a hash of safe fields and their values. This method
|
120
117
|
# calls the callables defined in the safe_dump_field_map with
|
121
118
|
# the instance object as an argument.
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# lib/familia/features/transient_fields/redacted_string.rb
|
2
|
+
|
3
|
+
# RedactedString
|
4
|
+
#
|
5
|
+
# A secure wrapper for sensitive string values (e.g., API keys, passwords,
|
6
|
+
# encryption keys).
|
7
|
+
# Designed to:
|
8
|
+
# - Prevent accidental logging/inspection
|
9
|
+
# - Enable secure memory wiping
|
10
|
+
# - Encourage safe usage patterns
|
11
|
+
#
|
12
|
+
# ⚠️ IMPORTANT: This is *best-effort* protection. Ruby does not guarantee
|
13
|
+
# memory zeroing. GC, string sharing, and internal optimizations
|
14
|
+
# may leave copies in memory.
|
15
|
+
#
|
16
|
+
# ⚠️ INPUT SECURITY: The constructor calls .dup on the input, creating a copy,
|
17
|
+
# but the original input value remains in memory uncontrolled.
|
18
|
+
# The caller is responsible for securely clearing the original.
|
19
|
+
#
|
20
|
+
# Security Model:
|
21
|
+
# - The secret is *contained* from the moment it's wrapped.
|
22
|
+
# - Access is available via `.expose { }` for controlled use, or `.value` for direct access.
|
23
|
+
# - Manual `.clear!` is required when done with the value (unlike SingleUseRedactedString).
|
24
|
+
# - `.to_s` and `.inspect` return '[REDACTED]' to prevent leaks in logs,
|
25
|
+
# errors, or debugging.
|
26
|
+
#
|
27
|
+
# Critical Gotchas:
|
28
|
+
#
|
29
|
+
# 1. Ruby 3.4+ String Internals — Memory Safety Reality
|
30
|
+
# - Ruby uses "compact strings" and copy-on-write semantics.
|
31
|
+
# - Short strings (< 24 bytes on 64-bit) are *embedded* in the object
|
32
|
+
# (RSTRING_EMBED_LEN).
|
33
|
+
# - Long strings use heap-allocated buffers, but may be shared or
|
34
|
+
# duplicated silently.
|
35
|
+
# - There is *no guarantee* that GC will not copy the string before
|
36
|
+
# finalization.
|
37
|
+
#
|
38
|
+
# 2. Every .dup, .to_s, +, interpolation, or method call may create hidden
|
39
|
+
# copies:
|
40
|
+
# s = "secret"
|
41
|
+
# t = s.dup # New object, same content — now two copies
|
42
|
+
# u = s + "123" # New string — third copy
|
43
|
+
# "#{t}" # Interpolation — fourth copy
|
44
|
+
# These copies are *not* controlled by RedactedString and may persist.
|
45
|
+
#
|
46
|
+
# 3. String Freezing & Immutability
|
47
|
+
# - `.freeze` prevents mutation but does *not* prevent copying.
|
48
|
+
# - `.replace` on a frozen string raises FrozenError — so wiping fails.
|
49
|
+
#
|
50
|
+
# 4. RbNaCl::Util.zero Limitations
|
51
|
+
# - Only works on mutable byte buffers.
|
52
|
+
# - May not zero embedded strings if Ruby's internal representation is
|
53
|
+
# immutable.
|
54
|
+
# - Does *not* protect against memory dumps or GC-compacted heaps.
|
55
|
+
#
|
56
|
+
# 5. Finalizers Are Not Guaranteed
|
57
|
+
# - Ruby does not promise when (or if) `ObjectSpace.define_finalizer`
|
58
|
+
# runs.
|
59
|
+
# - Never rely on finalizers for security-critical wiping.
|
60
|
+
#
|
61
|
+
# Best Practices:
|
62
|
+
# - Wrap secrets *immediately* on input (e.g., from ENV, params, DB).
|
63
|
+
# - Clear original input after wrapping: `secret.clear!` or `secret = nil`
|
64
|
+
# - Use `.expose { }` for short-lived operations — never store plaintext.
|
65
|
+
# - Avoid passing RedactedString to logging, serialization, or debugging
|
66
|
+
# tools.
|
67
|
+
# - Prefer `.expose { }` over any "getter" method.
|
68
|
+
# - Do *not* subclass String — it leaks the underlying value in regex,
|
69
|
+
# case, etc.
|
70
|
+
#
|
71
|
+
# Example:
|
72
|
+
# password_input = params[:password] # Original value in memory
|
73
|
+
# password = RedactedString.new(password_input)
|
74
|
+
# password_input.clear! if password_input.respond_to?(:clear!)
|
75
|
+
# # or: params[:password] = nil # Clear reference (not guaranteed)
|
76
|
+
#
|
77
|
+
class RedactedString
|
78
|
+
# Wrap a sensitive value. The input is *not* wiped — ensure it's not reused.
|
79
|
+
def initialize(original_value)
|
80
|
+
# WARNING: .dup only creates a shallow copy; the original may still exist
|
81
|
+
# elsewhere in memory.
|
82
|
+
@value = original_value.to_s.dup
|
83
|
+
@cleared = false
|
84
|
+
# Do NOT freeze — we need to mutate it in `#clear!`
|
85
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer_proc)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Primary API: expose the value in a block.
|
89
|
+
# The value remains accessible for multiple reads until manually cleared.
|
90
|
+
# Call clear! explicitly when done with the value.
|
91
|
+
#
|
92
|
+
# ⚠️ Security Warning: Avoid .dup, string interpolation, or other operations
|
93
|
+
# that create uncontrolled copies of the sensitive value.
|
94
|
+
#
|
95
|
+
# Example:
|
96
|
+
# token.expose do |plain|
|
97
|
+
# # Good: use directly without copying
|
98
|
+
# HTTP.post('/api', headers: { 'X-Token' => plain })
|
99
|
+
# # Avoid: plain.dup, "prefix#{plain}", plain[0..-1], etc.
|
100
|
+
# end
|
101
|
+
# # Value is still accessible after block
|
102
|
+
# token.clear! # Explicitly clear when done
|
103
|
+
#
|
104
|
+
def expose
|
105
|
+
raise ArgumentError, 'Block required' unless block_given?
|
106
|
+
raise SecurityError, 'Value already cleared' if cleared?
|
107
|
+
|
108
|
+
yield @value
|
109
|
+
end
|
110
|
+
|
111
|
+
# Clear the internal buffer. Safe to call multiple times.
|
112
|
+
#
|
113
|
+
# REALITY CHECK: This doesn't actually provide security in Ruby.
|
114
|
+
# - Ruby may have already copied the string elsewhere in memory
|
115
|
+
# - Garbage collection behavior is unpredictable
|
116
|
+
# - The original input value is still in memory somewhere
|
117
|
+
# - This is primarily for API consistency and preventing reuse
|
118
|
+
def clear!
|
119
|
+
return if @value.nil? || @value.frozen? || @cleared
|
120
|
+
|
121
|
+
# Simple clear - no security theater
|
122
|
+
@value.clear if @value.respond_to?(:clear)
|
123
|
+
@value = nil
|
124
|
+
@cleared = true
|
125
|
+
freeze # one and done
|
126
|
+
end
|
127
|
+
|
128
|
+
# Get the actual value (for convenience in less sensitive contexts)
|
129
|
+
# Returns the wrapped value or nil if cleared
|
130
|
+
#
|
131
|
+
# ⚠️ Security Warning: Direct access bypasses the controlled exposure pattern.
|
132
|
+
# Prefer .expose { } for better security practices.
|
133
|
+
def value
|
134
|
+
raise SecurityError, 'Value already cleared' if cleared?
|
135
|
+
|
136
|
+
@value
|
137
|
+
end
|
138
|
+
|
139
|
+
# Always redact in logs, debugging, or string conversion
|
140
|
+
def to_s = '[REDACTED]'
|
141
|
+
def inspect = to_s
|
142
|
+
def cleared? = @cleared
|
143
|
+
|
144
|
+
# Returns true when it's literally the same object, otherwsie false.
|
145
|
+
# This prevents timing attacks where an attacker could potentially
|
146
|
+
# infer information about the secret value through comparison timing
|
147
|
+
def ==(other)
|
148
|
+
object_id.equal?(other.object_id) # same object
|
149
|
+
end
|
150
|
+
alias eql? ==
|
151
|
+
|
152
|
+
# All RedactedString instances have the same hash to prevent
|
153
|
+
# hash-based timing attacks or information leakage
|
154
|
+
def hash
|
155
|
+
RedactedString.hash
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.finalizer_proc = proc { |id| }
|
159
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# lib/familia/features/transient_fields/single_use_redacted_string.rb
|
2
|
+
|
3
|
+
require_relative 'redacted_string'
|
4
|
+
|
5
|
+
# SingleUseRedactedString
|
6
|
+
#
|
7
|
+
# A high-security variant of RedactedString that automatically clears
|
8
|
+
# its value after a single use via the expose method. Unlike RedactedString,
|
9
|
+
# this provides automatic cleanup and enforces single-use semantics.
|
10
|
+
#
|
11
|
+
# ⚠️ IMPORTANT: Inherits all security limitations from RedactedString regarding
|
12
|
+
# Ruby's memory management and copy-on-write semantics.
|
13
|
+
#
|
14
|
+
# ⚠️ INPUT SECURITY: Like RedactedString, the constructor calls .dup on input,
|
15
|
+
# creating a copy, but the original input remains in memory.
|
16
|
+
# The caller is responsible for securely clearing the original.
|
17
|
+
#
|
18
|
+
# Key Differences from RedactedString:
|
19
|
+
# - Automatically clears after expose() (no manual clear! needed)
|
20
|
+
# - Blocks direct value() access (prevents accidental multi-use)
|
21
|
+
# - Raises SecurityError on second expose() attempt
|
22
|
+
#
|
23
|
+
# Use this for extremely sensitive values that should only be accessed
|
24
|
+
# once, such as:
|
25
|
+
# - One-time passwords (OTPs)
|
26
|
+
# - Temporary authentication tokens
|
27
|
+
# - Encryption keys that should be immediately discarded
|
28
|
+
#
|
29
|
+
# Example:
|
30
|
+
# otp_input = params[:otp] # Original value in memory
|
31
|
+
# otp = SingleUseRedactedString.new(otp_input)
|
32
|
+
# params[:otp] = nil # Clear reference (not guaranteed)
|
33
|
+
# otp.expose do |code|
|
34
|
+
# verify_otp(code) # Use directly without copying
|
35
|
+
# end
|
36
|
+
# # Value is automatically cleared after block
|
37
|
+
# otp.cleared? #=> true
|
38
|
+
#
|
39
|
+
class SingleUseRedactedString < RedactedString
|
40
|
+
# Override expose to automatically clear after use
|
41
|
+
#
|
42
|
+
# This ensures the value can only be accessed once via expose,
|
43
|
+
# providing maximum security for single-use secrets.
|
44
|
+
#
|
45
|
+
def expose
|
46
|
+
raise ArgumentError, 'Block required' unless block_given?
|
47
|
+
raise SecurityError, 'Value already cleared' if cleared?
|
48
|
+
|
49
|
+
yield @value
|
50
|
+
ensure
|
51
|
+
clear! # Automatically clear after single use
|
52
|
+
end
|
53
|
+
|
54
|
+
# Override value accessor to prevent direct access
|
55
|
+
#
|
56
|
+
# For single-use secrets, we don't want to allow direct value access
|
57
|
+
# to maintain the single-use guarantee.
|
58
|
+
#
|
59
|
+
def value
|
60
|
+
raise SecurityError, 'Direct value access not allowed for single-use secrets. Use #expose with a block.'
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# lib/familia/features/transient_fields/transient_field_type.rb
|
2
|
+
|
3
|
+
require 'familia/field_type'
|
4
|
+
|
5
|
+
require_relative 'redacted_string'
|
6
|
+
|
7
|
+
module Familia
|
8
|
+
# TransientFieldType - Fields that are not persisted to database
|
9
|
+
#
|
10
|
+
# Transient fields automatically wrap values in RedactedString for security
|
11
|
+
# and are excluded from serialization operations. They are ideal for storing
|
12
|
+
# sensitive data like API keys, passwords, and tokens that should not be
|
13
|
+
# persisted to the database.
|
14
|
+
#
|
15
|
+
# @example Using transient fields
|
16
|
+
# class SecretService < Familia::Horreum
|
17
|
+
# field :name # Regular field
|
18
|
+
# transient_field :api_key # Wrapped in RedactedString
|
19
|
+
# transient_field :password # Not persisted to database
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# service = SecretService.new
|
23
|
+
# service.api_key = "sk-1234567890"
|
24
|
+
# service.api_key.class #=> RedactedString
|
25
|
+
# service.to_h #=> {:name => nil} (no api_key)
|
26
|
+
#
|
27
|
+
class TransientFieldType < FieldType
|
28
|
+
# Override setter to wrap values in RedactedString
|
29
|
+
#
|
30
|
+
# Values are automatically wrapped in RedactedString objects for security.
|
31
|
+
# Nil values and existing RedactedString objects are handled appropriately.
|
32
|
+
#
|
33
|
+
# @param klass [Class] The class to define the method on
|
34
|
+
#
|
35
|
+
def define_setter(klass)
|
36
|
+
field_name = @name
|
37
|
+
method_name = @method_name
|
38
|
+
|
39
|
+
handle_method_conflict(klass, :"#{method_name}=") do
|
40
|
+
klass.define_method :"#{method_name}=" do |value|
|
41
|
+
wrapped = if value.nil?
|
42
|
+
nil
|
43
|
+
elsif value.is_a?(RedactedString)
|
44
|
+
value
|
45
|
+
else
|
46
|
+
RedactedString.new(value)
|
47
|
+
end
|
48
|
+
instance_variable_set(:"@#{field_name}", wrapped)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Override getter to unwrap RedactedString values
|
54
|
+
#
|
55
|
+
# Returns the actual value from the RedactedString wrapper for
|
56
|
+
# convenient access, or nil if the value is nil or cleared.
|
57
|
+
#
|
58
|
+
# @param klass [Class] The class to define the method on
|
59
|
+
#
|
60
|
+
def define_getter(klass)
|
61
|
+
field_name = @name
|
62
|
+
method_name = @method_name
|
63
|
+
|
64
|
+
handle_method_conflict(klass, method_name) do
|
65
|
+
klass.define_method method_name do
|
66
|
+
wrapped = instance_variable_get(:"@#{field_name}")
|
67
|
+
return nil if wrapped.nil? || wrapped.cleared?
|
68
|
+
|
69
|
+
wrapped
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Override fast writer to disable it for transient fields
|
75
|
+
#
|
76
|
+
# Transient fields should not have fast writers since they're not
|
77
|
+
# persisted to the database.
|
78
|
+
#
|
79
|
+
# @param klass [Class] The class to define the method on
|
80
|
+
#
|
81
|
+
def define_fast_writer(_klass)
|
82
|
+
# No fast writer for transient fields since they're not persisted
|
83
|
+
Familia.ld "[TransientFieldType] Skipping fast writer for transient field: #{@name}"
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
# Transient fields are not persisted to database
|
88
|
+
#
|
89
|
+
# @return [Boolean] false - transient fields are never persisted
|
90
|
+
#
|
91
|
+
def persistent?
|
92
|
+
false
|
93
|
+
end
|
94
|
+
|
95
|
+
# A convenience method that wraps `persistent?`
|
96
|
+
#
|
97
|
+
def transient?
|
98
|
+
!persistent?
|
99
|
+
end
|
100
|
+
|
101
|
+
# Category for transient fields
|
102
|
+
#
|
103
|
+
# @return [Symbol] :transient
|
104
|
+
#
|
105
|
+
def category
|
106
|
+
:transient
|
107
|
+
end
|
108
|
+
|
109
|
+
# Transient fields are not serialized to database
|
110
|
+
#
|
111
|
+
# This method should not be called since transient fields are not
|
112
|
+
# persisted, but we provide it for completeness.
|
113
|
+
#
|
114
|
+
# @param value [Object] The value to serialize
|
115
|
+
# @param record [Object] The record instance
|
116
|
+
# @return [nil] Always nil since transient fields are not serialized
|
117
|
+
#
|
118
|
+
def serialize(_value, _record = nil)
|
119
|
+
# Transient fields should never be serialized
|
120
|
+
Familia.ld "[TransientFieldType] WARNING: serialize called on transient field #{@name}"
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
|
124
|
+
# Transient fields are not deserialized from database
|
125
|
+
#
|
126
|
+
# This method should not be called since transient fields are not
|
127
|
+
# persisted, but we provide it for completeness.
|
128
|
+
#
|
129
|
+
# @param value [Object] The value to deserialize
|
130
|
+
# @param record [Object] The record instance
|
131
|
+
# @return [nil] Always nil since transient fields are not stored
|
132
|
+
#
|
133
|
+
def deserialize(_value, _record = nil)
|
134
|
+
# Transient fields should never be deserialized
|
135
|
+
Familia.ld "[TransientFieldType] WARNING: deserialize called on transient field #{@name}"
|
136
|
+
nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# lib/familia/features/transient_fields.rb
|
2
|
+
|
3
|
+
require_relative 'transient_fields/redacted_string'
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
module Features
|
7
|
+
# Familia::Features::TransientFields
|
8
|
+
#
|
9
|
+
# Provides secure transient fields that wrap sensitive values in RedactedString
|
10
|
+
# objects. These fields are excluded from serialization operations and provide
|
11
|
+
# automatic memory wiping for security.
|
12
|
+
#
|
13
|
+
module TransientFields
|
14
|
+
def self.included(base)
|
15
|
+
Familia.ld "[#{base}] Loaded #{self}"
|
16
|
+
base.extend ClassMethods
|
17
|
+
end
|
18
|
+
|
19
|
+
# ClassMethods
|
20
|
+
#
|
21
|
+
module ClassMethods
|
22
|
+
# Define a transient field that automatically wraps values in RedactedString
|
23
|
+
#
|
24
|
+
# @param name [Symbol] The field name
|
25
|
+
# @param as [Symbol] The method name (defaults to field name)
|
26
|
+
# @param kwargs [Hash] Additional field options
|
27
|
+
#
|
28
|
+
# @example Define a transient API key field
|
29
|
+
# class Service < Familia::Horreum
|
30
|
+
# feature :transient_fields
|
31
|
+
# transient_field :api_key
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
def transient_field(name, as: name, **kwargs)
|
35
|
+
# Use the field type system - much cleaner than alias_method approach!
|
36
|
+
# We can now remove the transient_field method from this feature entirely
|
37
|
+
# since it's built into DefinitionMethods using TransientFieldType
|
38
|
+
require_relative 'transient_fields/transient_field_type'
|
39
|
+
field_type = TransientFieldType.new(name, as: as, **kwargs.merge(fast_method: false))
|
40
|
+
register_field_type(field_type)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Familia::Base.add_feature self, :transient_fields, depends_on: nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/familia/features.rb
CHANGED
@@ -2,45 +2,61 @@
|
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
|
5
|
+
FeatureDefinition = Data.define(:name, :depends_on)
|
6
|
+
|
7
|
+
# Familia::Features
|
8
|
+
#
|
5
9
|
module Features
|
6
10
|
|
7
11
|
@features_enabled = nil
|
8
12
|
attr_reader :features_enabled
|
9
13
|
|
10
|
-
def feature(
|
14
|
+
def feature(feature_name = nil)
|
11
15
|
@features_enabled ||= []
|
12
16
|
|
13
|
-
|
14
|
-
if val
|
15
|
-
val = val.to_sym
|
16
|
-
raise Familia::Problem, "Unsupported feature: #{val}" unless Familia::Base.features.key?(val)
|
17
|
+
return features_enabled if feature_name.nil?
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
# If there's a value provied check that it's a valid feature
|
20
|
+
feature_name = feature_name.to_sym
|
21
|
+
unless Familia::Base.features_available.key?(feature_name)
|
22
|
+
raise Familia::Problem, "Unsupported feature: #{feature_name}"
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
+
# If the feature is already available, do nothing but log about it
|
26
|
+
if features_enabled.member?(feature_name)
|
27
|
+
Familia.warn "[#{self.class}] feature already available: #{feature_name}"
|
28
|
+
return
|
29
|
+
end
|
25
30
|
|
26
|
-
|
31
|
+
if Familia.debug?
|
32
|
+
Familia.trace :FEATURE, nil, "#{self} includes #{feature_name.inspect}", caller(1..1)
|
33
|
+
end
|
27
34
|
|
28
|
-
|
29
|
-
|
35
|
+
# Add it to the list available features_enabled for Familia::Base classes.
|
36
|
+
features_enabled << feature_name
|
30
37
|
|
31
|
-
|
32
|
-
# call safe_dump on relations fields (e.g. list, set, zset, hashkey). Or
|
33
|
-
# maybe that only makes sense for hashk/object relations.
|
34
|
-
#
|
35
|
-
# We'd need to avoid it getting included multiple times (i.e. once for each
|
36
|
-
# Familia::Horreum subclass that includes the feature).
|
38
|
+
klass = Familia::Base.features_available[feature_name]
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
|
40
|
+
# Validate dependencies
|
41
|
+
feature_def = Familia::Base.feature_definitions[feature_name]
|
42
|
+
if feature_def&.depends_on&.any?
|
43
|
+
missing = feature_def.depends_on - features_enabled
|
44
|
+
raise Familia::Problem, "#{feature_name} requires: #{missing.join(', ')}" if missing.any?
|
41
45
|
end
|
42
46
|
|
43
|
-
|
47
|
+
# Extend the Familia::Base subclass (e.g. Customer) with the feature module
|
48
|
+
include klass
|
49
|
+
|
50
|
+
# NOTE: Do we want to extend Familia::DataType here? That would make it
|
51
|
+
# possible to call safe_dump on relations fields (e.g. list, zset, hashkey).
|
52
|
+
#
|
53
|
+
# The challenge is that DataType classes (List, Set, etc.) are shared across
|
54
|
+
# all Horreum models. If Customer extends DataType with safe_dump, then
|
55
|
+
# Session's lists would also have it. Not ideal. If that's all we wanted
|
56
|
+
# then we can do that by looping through every DataType class here.
|
57
|
+
#
|
58
|
+
# We'd need to extend the DataType instances for each Horreum subclass. That
|
59
|
+
# avoids it getting included multiple times per DataType
|
44
60
|
end
|
45
61
|
|
46
62
|
end
|