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
@@ -0,0 +1,183 @@
|
|
1
|
+
# Security Model
|
2
|
+
|
3
|
+
## Cryptographic Design
|
4
|
+
|
5
|
+
### Provider-Based Architecture
|
6
|
+
|
7
|
+
Familia uses a modular provider system that automatically selects the best available encryption algorithm:
|
8
|
+
|
9
|
+
### Encryption Algorithms
|
10
|
+
|
11
|
+
**XChaCha20-Poly1305 Provider (Priority: 100)**
|
12
|
+
- Requires: `rbnacl` gem (libsodium bindings)
|
13
|
+
- Key Size: 256 bits (32 bytes)
|
14
|
+
- Nonce Size: 192 bits (24 bytes) - extended nonce space
|
15
|
+
- Authentication Tag: 128 bits (16 bytes)
|
16
|
+
- Key Derivation: BLAKE2b with personalization string
|
17
|
+
|
18
|
+
**AES-256-GCM Provider (Priority: 50)**
|
19
|
+
- Requires: OpenSSL (always available)
|
20
|
+
- Key Size: 256 bits (32 bytes)
|
21
|
+
- Nonce Size: 96 bits (12 bytes) - standard GCM nonce
|
22
|
+
- Authentication Tag: 128 bits (16 bytes)
|
23
|
+
- Key Derivation: HKDF-SHA256
|
24
|
+
|
25
|
+
### Key Derivation
|
26
|
+
|
27
|
+
Each field gets a unique key derived from the master key:
|
28
|
+
|
29
|
+
```
|
30
|
+
Field Key = KDF(Master Key, Context)
|
31
|
+
|
32
|
+
Where Context = "ClassName:field_name:record_identifier"
|
33
|
+
```
|
34
|
+
|
35
|
+
**Provider-Specific KDF:**
|
36
|
+
- **XChaCha20-Poly1305**: BLAKE2b with customizable personalization string
|
37
|
+
- **AES-256-GCM**: HKDF-SHA256 with salt and info parameters
|
38
|
+
|
39
|
+
The personalization string provides cryptographic domain separation:
|
40
|
+
```ruby
|
41
|
+
Familia.configure do |config|
|
42
|
+
config.encryption_personalization = 'MyApp-2024' # Default: 'Familia'
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
### Ciphertext Format
|
47
|
+
|
48
|
+
The encrypted data is stored as JSON with algorithm-specific fields:
|
49
|
+
|
50
|
+
**XChaCha20-Poly1305:**
|
51
|
+
```json
|
52
|
+
{
|
53
|
+
"algorithm": "xchacha20poly1305",
|
54
|
+
"nonce": "base64_24_byte_nonce",
|
55
|
+
"ciphertext": "base64_encrypted_data",
|
56
|
+
"auth_tag": "base64_16_byte_tag",
|
57
|
+
"key_version": "v1"
|
58
|
+
}
|
59
|
+
```
|
60
|
+
|
61
|
+
**AES-256-GCM:**
|
62
|
+
```json
|
63
|
+
{
|
64
|
+
"algorithm": "aes-256-gcm",
|
65
|
+
"nonce": "base64_12_byte_iv",
|
66
|
+
"ciphertext": "base64_encrypted_data",
|
67
|
+
"auth_tag": "base64_16_byte_tag",
|
68
|
+
"key_version": "v1"
|
69
|
+
}
|
70
|
+
```
|
71
|
+
|
72
|
+
## Threat Model
|
73
|
+
|
74
|
+
### Protected Against
|
75
|
+
|
76
|
+
#### Database Compromise
|
77
|
+
- All sensitive fields encrypted with strong keys
|
78
|
+
- Attackers see only ciphertext
|
79
|
+
|
80
|
+
#### Field Value Swapping
|
81
|
+
- Field-specific key derivation prevents cross-field decryption
|
82
|
+
- Swapped values fail to decrypt
|
83
|
+
|
84
|
+
#### Replay Attacks
|
85
|
+
- Each encryption uses unique random nonce
|
86
|
+
- Old values remain valid but are distinct encryptions
|
87
|
+
|
88
|
+
#### Tampering
|
89
|
+
- Authenticated encryption (Poly1305/GCM)
|
90
|
+
- Modified ciphertext fails authentication
|
91
|
+
|
92
|
+
### Not Protected Against
|
93
|
+
|
94
|
+
#### Application Memory Compromise
|
95
|
+
- Plaintext values exist in Ruby memory
|
96
|
+
- Mitigation: Use libsodium for memory wiping, minimize plaintext lifetime
|
97
|
+
|
98
|
+
#### Master Key Compromise
|
99
|
+
- All encrypted data compromised if keys obtained
|
100
|
+
- Mitigation: Secure key storage, regular rotation, hardware security modules
|
101
|
+
|
102
|
+
#### Side-Channel Attacks
|
103
|
+
- Key recovery through timing/power analysis
|
104
|
+
- Mitigation: Libsodium provides constant-time operations
|
105
|
+
|
106
|
+
## Additional Security Features
|
107
|
+
|
108
|
+
### Passphrase Protection
|
109
|
+
|
110
|
+
For ultra-sensitive fields, add user passphrases:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
encrypted_field :love_letter
|
114
|
+
|
115
|
+
# Passphrase required for decryption
|
116
|
+
vault.love_letter(passphrase_value: user_passphrase)
|
117
|
+
```
|
118
|
+
|
119
|
+
**How it works:**
|
120
|
+
1. Passphrase hashed with SHA-256
|
121
|
+
2. Hash included in Additional Authenticated Data (AAD)
|
122
|
+
3. Wrong passphrase = authentication failure
|
123
|
+
4. Passphrase never stored, only verified
|
124
|
+
|
125
|
+
### Memory Safety
|
126
|
+
|
127
|
+
**⚠️ Critical Ruby Memory Limitations:**
|
128
|
+
|
129
|
+
Ruby provides **NO** memory safety guarantees for cryptographic secrets. This affects ALL providers:
|
130
|
+
|
131
|
+
- **No secure memory wiping**: Ruby cannot guarantee memory zeroing
|
132
|
+
- **GC copying**: Garbage collector may copy secrets before cleanup
|
133
|
+
- **String operations**: Every `.dup`, `+`, or interpolation creates uncontrolled copies
|
134
|
+
- **Memory dumps**: Secrets may persist in swap files or core dumps
|
135
|
+
- **Finalizer uncertainty**: `ObjectSpace.define_finalizer` timing is unpredictable
|
136
|
+
|
137
|
+
**Provider-Specific Mitigations:**
|
138
|
+
|
139
|
+
Both providers attempt best-effort memory clearing:
|
140
|
+
- Call `.clear` on sensitive strings after use
|
141
|
+
- Set variables to `nil` when done
|
142
|
+
- Use finalizers for cleanup (no guarantees)
|
143
|
+
|
144
|
+
**Recommendation**: For production systems with high-security requirements, consider:
|
145
|
+
- Hardware Security Modules (HSMs)
|
146
|
+
- External key management services
|
147
|
+
- Languages with manual memory management (C, Rust)
|
148
|
+
- Cryptographic appliances with secure enclaves
|
149
|
+
|
150
|
+
### RedactedString
|
151
|
+
|
152
|
+
Prevents accidental logging of sensitive data:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
class RedactedString < String
|
156
|
+
def to_s
|
157
|
+
'[REDACTED]'
|
158
|
+
end
|
159
|
+
|
160
|
+
def inspect
|
161
|
+
'[REDACTED]'
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# In logs:
|
166
|
+
logger.info "Love letter: #{user.love_letter}" # => "Love letter: [REDACTED]"
|
167
|
+
```
|
168
|
+
|
169
|
+
## Security Checklist
|
170
|
+
|
171
|
+
### Development
|
172
|
+
|
173
|
+
- [ ] Never log plaintext sensitive fields
|
174
|
+
- [ ] Use RedactedString for extra protection
|
175
|
+
- [ ] Use libsodium for production when possible
|
176
|
+
- [ ] Validate encryption at startup
|
177
|
+
- [ ] Test encryption round-trips
|
178
|
+
|
179
|
+
### Operations
|
180
|
+
|
181
|
+
- [ ] Regular key rotation schedule
|
182
|
+
- [ ] Monitor decryption failures
|
183
|
+
- [ ] Log field access patterns for auditing purposes
|
@@ -0,0 +1,280 @@
|
|
1
|
+
# Transient Fields Guide
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
Transient fields provide secure handling of sensitive runtime data that should never be persisted to Redis/Valkey. Unlike encrypted fields, transient fields exist only in memory and are automatically wrapped in `RedactedString` for security.
|
6
|
+
|
7
|
+
## When to Use Transient Fields
|
8
|
+
|
9
|
+
Use transient fields for:
|
10
|
+
- API keys and tokens that change frequently
|
11
|
+
- Temporary passwords or passphrases
|
12
|
+
- Session-specific secrets
|
13
|
+
- Any sensitive data that should never touch persistent storage
|
14
|
+
- Debug or development secrets that need secure handling
|
15
|
+
|
16
|
+
## Basic Usage
|
17
|
+
|
18
|
+
### Define Transient Fields
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
class ApiClient < Familia::Horreum
|
22
|
+
feature :transient_fields
|
23
|
+
|
24
|
+
field :endpoint # Regular persistent field
|
25
|
+
transient_field :token # Transient field (not persisted)
|
26
|
+
transient_field :secret, as: :api_secret # Custom accessor name
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
### Working with Transient Fields
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
client = ApiClient.new(
|
34
|
+
endpoint: 'https://api.example.com',
|
35
|
+
token: ENV['API_TOKEN'],
|
36
|
+
secret: ENV['API_SECRET']
|
37
|
+
)
|
38
|
+
|
39
|
+
# Regular field persists
|
40
|
+
client.save
|
41
|
+
client.endpoint # => "https://api.example.com"
|
42
|
+
|
43
|
+
# Transient fields are RedactedString instances
|
44
|
+
puts client.token # => "[REDACTED]"
|
45
|
+
|
46
|
+
# Access the actual value safely
|
47
|
+
client.token.expose do |token|
|
48
|
+
response = HTTP.post(client.endpoint,
|
49
|
+
headers: { 'Authorization' => "Bearer #{token}" }
|
50
|
+
)
|
51
|
+
# Token value is only available within this block
|
52
|
+
end
|
53
|
+
|
54
|
+
# Explicit cleanup when done
|
55
|
+
client.token.clear!
|
56
|
+
```
|
57
|
+
|
58
|
+
## RedactedString Security
|
59
|
+
|
60
|
+
### Automatic Wrapping
|
61
|
+
|
62
|
+
All transient field values are automatically wrapped in `RedactedString`:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
client = ApiClient.new(token: 'secret123')
|
66
|
+
client.token.class # => RedactedString
|
67
|
+
```
|
68
|
+
|
69
|
+
### Safe Access Pattern
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# ✅ Recommended: Use .expose block
|
73
|
+
client.token.expose do |token|
|
74
|
+
# Use token directly without creating copies
|
75
|
+
HTTP.auth("Bearer #{token}") # Safe
|
76
|
+
end
|
77
|
+
|
78
|
+
# ✅ Direct access (use carefully)
|
79
|
+
raw_token = client.token.value
|
80
|
+
# Remember to clear original source if needed
|
81
|
+
|
82
|
+
# ❌ Avoid: These create uncontrolled copies
|
83
|
+
token_copy = client.token.value.dup # Creates copy in memory
|
84
|
+
interpolated = "Bearer #{client.token}" # Creates copy via to_s
|
85
|
+
```
|
86
|
+
|
87
|
+
### Memory Management
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
# Clear individual fields
|
91
|
+
client.token.clear!
|
92
|
+
|
93
|
+
# Check if cleared
|
94
|
+
client.token.cleared? # => true
|
95
|
+
|
96
|
+
# Accessing cleared values raises error
|
97
|
+
client.token.value # => SecurityError: Value already cleared
|
98
|
+
```
|
99
|
+
|
100
|
+
## Advanced Features
|
101
|
+
|
102
|
+
### Custom Accessor Names
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class Service < Familia::Horreum
|
106
|
+
transient_field :api_key, as: :secret_key
|
107
|
+
end
|
108
|
+
|
109
|
+
service = Service.new(api_key: 'secret123')
|
110
|
+
service.secret_key.expose { |key| use_api_key(key) }
|
111
|
+
```
|
112
|
+
|
113
|
+
### Integration with Encrypted Fields
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class SecureService < Familia::Horreum
|
117
|
+
feature :transient_fields
|
118
|
+
|
119
|
+
encrypted_field :long_term_secret # Persisted, encrypted
|
120
|
+
transient_field :session_token # Runtime only, not persisted
|
121
|
+
field :public_endpoint # Normal field
|
122
|
+
end
|
123
|
+
|
124
|
+
service = SecureService.new(
|
125
|
+
long_term_secret: 'stored encrypted in Redis',
|
126
|
+
session_token: 'temporary runtime secret',
|
127
|
+
public_endpoint: 'https://api.example.com'
|
128
|
+
)
|
129
|
+
|
130
|
+
service.save
|
131
|
+
# Only long_term_secret and public_endpoint are saved to Redis
|
132
|
+
# session_token exists only in memory
|
133
|
+
```
|
134
|
+
|
135
|
+
## RedactedString API Reference
|
136
|
+
|
137
|
+
### Core Methods
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# Create (usually automatic via transient_field)
|
141
|
+
secret = RedactedString.new('sensitive_value')
|
142
|
+
|
143
|
+
# Safe access
|
144
|
+
secret.expose { |value| use_value(value) }
|
145
|
+
|
146
|
+
# Direct access (use with caution)
|
147
|
+
value = secret.value
|
148
|
+
|
149
|
+
# Cleanup
|
150
|
+
secret.clear!
|
151
|
+
|
152
|
+
# Status
|
153
|
+
secret.cleared? # => true/false
|
154
|
+
```
|
155
|
+
|
156
|
+
### Security Methods
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
# Logging/debugging protection
|
160
|
+
puts secret.to_s # => "[REDACTED]"
|
161
|
+
puts secret.inspect # => "[REDACTED]"
|
162
|
+
|
163
|
+
# Equality (object identity only)
|
164
|
+
secret1 == secret2 # => false (unless same object)
|
165
|
+
|
166
|
+
# Hash (constant for all instances)
|
167
|
+
secret.hash # => Same for all RedactedString instances
|
168
|
+
```
|
169
|
+
|
170
|
+
## Security Considerations
|
171
|
+
|
172
|
+
### Ruby Memory Limitations
|
173
|
+
|
174
|
+
**⚠️ Important**: Ruby provides no memory safety guarantees:
|
175
|
+
|
176
|
+
- **No secure wiping**: `.clear!` is best-effort only
|
177
|
+
- **GC copying**: Garbage collector may duplicate secrets
|
178
|
+
- **String operations**: Every manipulation creates copies
|
179
|
+
- **Memory persistence**: Secrets may remain in memory indefinitely
|
180
|
+
|
181
|
+
### Best Practices
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
# ✅ Wrap immediately after input
|
185
|
+
password = RedactedString.new(params[:password])
|
186
|
+
params[:password] = nil # Clear original reference
|
187
|
+
|
188
|
+
# ✅ Use .expose for short operations
|
189
|
+
token.expose { |t| api_call(t) }
|
190
|
+
|
191
|
+
# ✅ Clear explicitly when done
|
192
|
+
token.clear!
|
193
|
+
|
194
|
+
# ✅ Avoid string operations that create copies
|
195
|
+
token.expose { |t| "Bearer #{t}" } # Creates copy
|
196
|
+
# Better: Pass token directly to methods that need it
|
197
|
+
|
198
|
+
# ❌ Don't pass RedactedString to logging
|
199
|
+
logger.info "Token: #{token}" # Still logs "[REDACTED]" but safer to avoid
|
200
|
+
|
201
|
+
# ❌ Don't store in instance variables outside field system
|
202
|
+
@raw_token = token.value # Creates uncontrolled copy
|
203
|
+
```
|
204
|
+
|
205
|
+
### Production Recommendations
|
206
|
+
|
207
|
+
For highly sensitive applications, consider:
|
208
|
+
- External secrets management (HashiCorp Vault, AWS Secrets Manager)
|
209
|
+
- Hardware Security Modules (HSMs)
|
210
|
+
- Languages with secure memory handling
|
211
|
+
- Encrypted swap and memory protection at OS level
|
212
|
+
|
213
|
+
## Integration Examples
|
214
|
+
|
215
|
+
### Rails Controller
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
class ApiController < ApplicationController
|
219
|
+
def authenticate
|
220
|
+
service = ApiService.new(
|
221
|
+
endpoint: params[:endpoint],
|
222
|
+
token: params[:token] # Auto-wrapped in RedactedString
|
223
|
+
)
|
224
|
+
|
225
|
+
result = service.token.expose do |token|
|
226
|
+
# Token only accessible within this block
|
227
|
+
ExternalAPI.authenticate(token)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Clear token when request is done
|
231
|
+
service.token.clear!
|
232
|
+
|
233
|
+
render json: { status: result }
|
234
|
+
end
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
### Background Job
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
class ApiSyncJob
|
242
|
+
def perform(user_id, token)
|
243
|
+
user = User.find(user_id)
|
244
|
+
|
245
|
+
# Wrap external token securely
|
246
|
+
secure_token = RedactedString.new(token)
|
247
|
+
token.clear if token.respond_to?(:clear) # Clear original
|
248
|
+
|
249
|
+
client = ApiClient.new(token: secure_token)
|
250
|
+
|
251
|
+
begin
|
252
|
+
sync_data(client)
|
253
|
+
ensure
|
254
|
+
client.token.clear! # Always cleanup
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
|
260
|
+
def sync_data(client)
|
261
|
+
client.token.expose do |token|
|
262
|
+
# Use token for API calls
|
263
|
+
fetch_and_process_data(token)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
## Comparison with Encrypted Fields
|
270
|
+
|
271
|
+
| Feature | Encrypted Fields | Transient Fields |
|
272
|
+
|---------|------------------|------------------|
|
273
|
+
| **Persistence** | Saved to Redis/Valkey | Memory only |
|
274
|
+
| **Encryption** | AES/XChaCha20 | None (not stored) |
|
275
|
+
| **Use Case** | Long-term secrets | Runtime secrets |
|
276
|
+
| **Access** | Automatic decrypt | RedactedString wrapper |
|
277
|
+
| **Performance** | Crypto overhead | No crypto operations |
|
278
|
+
| **Lifecycle** | Survives restarts | Cleared on restart |
|
279
|
+
|
280
|
+
Choose encrypted fields for data that must persist across sessions. Choose transient fields for sensitive runtime data that should never be stored.
|
data/lib/familia/base.rb
CHANGED
@@ -14,7 +14,8 @@ module Familia
|
|
14
14
|
# @see Familia::DataType
|
15
15
|
#
|
16
16
|
module Base
|
17
|
-
@
|
17
|
+
@features_available = nil
|
18
|
+
@feature_definitions = nil
|
18
19
|
@dump_method = :to_json
|
19
20
|
@load_method = :from_json
|
20
21
|
|
@@ -29,43 +30,33 @@ module Familia
|
|
29
30
|
end
|
30
31
|
|
31
32
|
class << self
|
32
|
-
attr_reader :
|
33
|
+
attr_reader :features_available, :feature_definitions
|
33
34
|
attr_accessor :dump_method, :load_method
|
34
35
|
|
35
|
-
def add_feature(klass,
|
36
|
-
@
|
37
|
-
Familia.
|
36
|
+
def add_feature(klass, feature_name, depends_on: [])
|
37
|
+
@features_available ||= {}
|
38
|
+
Familia.trace :ADD_FEATURE, klass, feature_name, caller(1..1) if Familia.debug?
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
# Create field definition object
|
41
|
+
feature_def = FeatureDefinition.new(
|
42
|
+
name: feature_name,
|
43
|
+
depends_on: depends_on,
|
44
|
+
)
|
42
45
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
#
|
50
|
-
# @param default_expiration [Integer, nil] Time To Live in seconds (ignored in base implementation)
|
51
|
-
# @return [nil] Always returns nil
|
52
|
-
#
|
53
|
-
# @note This is a no-op implementation. Classes that need expiration
|
54
|
-
# functionality should include the :expiration feature.
|
55
|
-
#
|
56
|
-
def update_expiration(default_expiration: nil)
|
57
|
-
Familia.ld "[update_expiration] Feature not enabled for #{self.class}. Key: #{dbkey} (caller: #{caller(1..1)})"
|
58
|
-
nil
|
46
|
+
# Track field definitions after defining field methods
|
47
|
+
@feature_definitions ||= {}
|
48
|
+
@feature_definitions[feature_name] = feature_def
|
49
|
+
|
50
|
+
features_available[feature_name] = klass
|
51
|
+
end
|
59
52
|
end
|
60
53
|
|
61
54
|
def generate_id
|
62
|
-
@identifier ||= Familia.generate_id
|
63
|
-
@identifier
|
55
|
+
@identifier ||= Familia.generate_id # rubocop:disable Naming/MemoizedInstanceVariableName
|
64
56
|
end
|
65
57
|
|
66
58
|
def uuid
|
67
59
|
@uuid ||= SecureRandom.uuid
|
68
|
-
@uuid
|
69
60
|
end
|
70
61
|
end
|
71
62
|
end
|
data/lib/familia/connection.rb
CHANGED
@@ -106,18 +106,19 @@ module Familia
|
|
106
106
|
# Always pass normalized URI with database to provider
|
107
107
|
# Provider MUST return connection already on the correct database
|
108
108
|
parsed_uri = normalize_uri(uri)
|
109
|
-
|
109
|
+
client = connection_provider.call(parsed_uri.to_s)
|
110
110
|
|
111
111
|
# In debug mode, verify the provider honored the contract
|
112
|
-
if Familia.debug? &&
|
113
|
-
current_db =
|
112
|
+
if Familia.debug? && client.respond_to?(:client)
|
113
|
+
current_db = client.connection[:db]
|
114
114
|
expected_db = parsed_uri.db || 0
|
115
|
+
Familia.ld "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
|
115
116
|
if current_db != expected_db
|
116
|
-
Familia.warn "Connection provider returned
|
117
|
+
Familia.warn "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
|
117
118
|
end
|
118
119
|
end
|
119
120
|
|
120
|
-
return
|
121
|
+
return client
|
121
122
|
end
|
122
123
|
|
123
124
|
# Third priority: Fallback behavior or error
|
@@ -1,11 +1,9 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/commands.rb
|
2
2
|
|
3
3
|
class Familia::DataType
|
4
|
-
|
5
4
|
# Must be included in all DataType classes to provide Redis
|
6
5
|
# commands. The class must have a dbkey method.
|
7
6
|
module Commands
|
8
|
-
|
9
7
|
def move(logical_database)
|
10
8
|
dbclient.move dbkey, logical_database
|
11
9
|
end
|
@@ -52,8 +50,7 @@ class Familia::DataType
|
|
52
50
|
end
|
53
51
|
|
54
52
|
def echo(meth, trace)
|
55
|
-
dbclient.echo "[#{self.class}
|
53
|
+
dbclient.echo "[#{self.class}##{meth}] #{trace} (#{@opts[:class]}#)"
|
56
54
|
end
|
57
|
-
|
58
55
|
end
|
59
56
|
end
|
@@ -1,9 +1,7 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/serialization.rb
|
2
2
|
|
3
3
|
class Familia::DataType
|
4
|
-
|
5
4
|
module Serialization
|
6
|
-
|
7
5
|
# Serializes a value for storage in Redis.
|
8
6
|
#
|
9
7
|
# @param val [Object] The value to be serialized.
|
@@ -33,7 +31,7 @@ class Familia::DataType
|
|
33
31
|
|
34
32
|
if opts[:class]
|
35
33
|
prepared = Familia.distinguisher(opts[:class], strict_values: strict_values)
|
36
|
-
Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared||'<nil>'}"
|
34
|
+
Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
|
37
35
|
end
|
38
36
|
|
39
37
|
if prepared.nil?
|
@@ -42,9 +40,12 @@ class Familia::DataType
|
|
42
40
|
Familia.ld " from <#{val.class}> => <#{prepared.class}>"
|
43
41
|
end
|
44
42
|
|
45
|
-
|
43
|
+
if Familia.debug?
|
44
|
+
Familia.trace :TOREDIS, dbclient, "#{val}<#{val.class}|#{opts[:class]}> => #{prepared}<#{prepared.class}>",
|
45
|
+
caller(1..1)
|
46
|
+
end
|
46
47
|
|
47
|
-
Familia.warn "[#{self.class}
|
48
|
+
Familia.warn "[#{self.class}#serialize_value] nil returned for #{opts[:class]}##{name}" if prepared.nil?
|
48
49
|
prepared
|
49
50
|
end
|
50
51
|
|
@@ -88,9 +89,7 @@ class Familia::DataType
|
|
88
89
|
next if obj.nil?
|
89
90
|
|
90
91
|
val = @opts[:class].send load_method, obj
|
91
|
-
if val.nil?
|
92
|
-
Familia.ld "[#{self.class}\#deserialize_values] nil returned for #{@opts[:class]}\##{name}"
|
93
|
-
end
|
92
|
+
Familia.ld "[#{self.class}#deserialize_values] nil returned for #{@opts[:class]}##{name}" if val.nil?
|
94
93
|
|
95
94
|
val
|
96
95
|
rescue StandardError => e
|
@@ -125,5 +124,4 @@ class Familia::DataType
|
|
125
124
|
ret&.first # return the object or nil
|
126
125
|
end
|
127
126
|
end
|
128
|
-
|
129
127
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# lib/familia/data_type/types/counter.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
class Counter < String
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
@opts[:default] ||= 0
|
8
|
+
end
|
9
|
+
|
10
|
+
# Enhanced counter semantics
|
11
|
+
def reset(val = 0)
|
12
|
+
set(val).to_s.eql?('OK')
|
13
|
+
end
|
14
|
+
|
15
|
+
def increment_if_less_than(threshold, amount = 1)
|
16
|
+
current = to_i
|
17
|
+
return false if current >= threshold
|
18
|
+
|
19
|
+
incrementby(amount)
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def atomic_increment_and_get(amount = 1)
|
24
|
+
incrementby(amount)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Override to ensure integer serialization
|
28
|
+
def value=(val)
|
29
|
+
super(val.to_i)
|
30
|
+
end
|
31
|
+
|
32
|
+
def value
|
33
|
+
super.to_i
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Familia::DataType.register Familia::Counter, :counter
|