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
data/docs/overview.md
ADDED
@@ -0,0 +1,359 @@
|
|
1
|
+
|
2
|
+
# Familia - Overview
|
3
|
+
|
4
|
+
> [!NOTE]
|
5
|
+
> This document refers to Valkey throughout, but all examples and patterns work identically with Redis. Familia supports both Valkey and Redis as they share the same protocol and data structures.
|
6
|
+
|
7
|
+
## Introduction
|
8
|
+
|
9
|
+
Familia is a Ruby ORM for Valkey (Redis) that provides object-oriented access to Valkey's native data structures. Unlike traditional ORMs that map objects to relational tables, Familia preserves Valkey's performance and flexibility while offering a familiar Ruby interface.
|
10
|
+
|
11
|
+
**Why Familia?**
|
12
|
+
- Maps Ruby objects directly to Valkey's native data structures (strings, lists, sets, etc.)
|
13
|
+
- Maintains Valkey's atomic operations and performance characteristics
|
14
|
+
- Handles complex patterns (quantization, encryption, expiration) out of the box
|
15
|
+
|
16
|
+
## Core Concepts
|
17
|
+
|
18
|
+
### What is a Horreum Class?
|
19
|
+
|
20
|
+
The ```Horreum``` class is Familia's foundation, representing Valkey-compatible objects. It's named after ancient Roman storehouses, reflecting its purpose as a structured data repository.
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class Flower < Familia::Horreum
|
24
|
+
identifier_field :token
|
25
|
+
field :name
|
26
|
+
field :color
|
27
|
+
field :species
|
28
|
+
list :owners
|
29
|
+
set :tags
|
30
|
+
zset :metrics
|
31
|
+
hashkey :props
|
32
|
+
string :counter
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
This pattern lets you work with Valkey data as Ruby objects while maintaining direct access to Valkey's native operations.
|
37
|
+
|
38
|
+
### Flexible Identifiers
|
39
|
+
|
40
|
+
Horreum classes require identifiers to determine Valkey key names. You can define them in various ways:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class User < Familia::Horreum
|
44
|
+
# Simple field-based identifier
|
45
|
+
identifier_field :email
|
46
|
+
|
47
|
+
# Computed identifier
|
48
|
+
identifier_field ->(user) { "user:#{user.id}" }
|
49
|
+
|
50
|
+
# Multi-field composite identifier
|
51
|
+
identifier_field [:type, :email]
|
52
|
+
|
53
|
+
field :email
|
54
|
+
field :type
|
55
|
+
field :id
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
This flexibility allows you to adapt to different Valkey key naming strategies while maintaining clean Ruby object interfaces.
|
60
|
+
|
61
|
+
### Data Types Mapping
|
62
|
+
|
63
|
+
Familia provides direct mappings to Valkey's native data structures:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class Product < Familia::Horreum
|
67
|
+
identifier_field :sku
|
68
|
+
|
69
|
+
# Basic fields
|
70
|
+
field :sku
|
71
|
+
field :name
|
72
|
+
field :price
|
73
|
+
|
74
|
+
# String fields (for counters, simple values)
|
75
|
+
string :view_count, default: '0'
|
76
|
+
# Usage: view_count.increment (atomic increment)
|
77
|
+
|
78
|
+
# Lists (ordered, allows duplicates)
|
79
|
+
list :categories
|
80
|
+
# Usage: categories.push('fruit'), categories.pop
|
81
|
+
|
82
|
+
# Sets (unordered, unique)
|
83
|
+
set :tags
|
84
|
+
# Usage: tags.add('organic'), tags.include?('organic')
|
85
|
+
|
86
|
+
# Sorted sets (scored, ordered)
|
87
|
+
zset :ratings
|
88
|
+
# Usage: ratings.add(4.5, 'customer123'), ratings.rank('customer123')
|
89
|
+
|
90
|
+
# Hash keys (dictionaries)
|
91
|
+
hashkey :attributes
|
92
|
+
# Usage: attributes['color'] = 'red', attributes.to_h
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
Each type maintains Valkey's native operations while providing Ruby-friendly interfaces.
|
97
|
+
|
98
|
+
## Essential Features
|
99
|
+
|
100
|
+
### Automatic Expiration
|
101
|
+
|
102
|
+
Set default TTL for objects that should expire:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class Session < Familia::Horreum
|
106
|
+
feature :expiration
|
107
|
+
default_expiration 30.minutes
|
108
|
+
|
109
|
+
field :user_id
|
110
|
+
field :token
|
111
|
+
end
|
112
|
+
|
113
|
+
# Auto-expires in 30 minutes
|
114
|
+
session = Session.create(user_id: '123', token: 'abc')
|
115
|
+
```
|
116
|
+
|
117
|
+
This is ideal for temporary data like authentication tokens or cache entries.
|
118
|
+
|
119
|
+
### Safe Dumping for APIs
|
120
|
+
|
121
|
+
Control which fields are exposed when serializing objects:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class User < Familia::Horreum
|
125
|
+
feature :safe_dump
|
126
|
+
|
127
|
+
@safe_dump_fields = [
|
128
|
+
:id,
|
129
|
+
:email,
|
130
|
+
{full_name: ->(user) { "#{user.first_name} #{user.last_name}" }}
|
131
|
+
]
|
132
|
+
|
133
|
+
field :id, :email, :first_name, :last_name, :password_hash
|
134
|
+
end
|
135
|
+
|
136
|
+
user.safe_dump
|
137
|
+
#=> {id: "123", email: "alice@example.com", full_name: "Alice Smith"}
|
138
|
+
```
|
139
|
+
|
140
|
+
Prevents accidental exposure of sensitive data in API responses.
|
141
|
+
|
142
|
+
### Time-based Quantization
|
143
|
+
|
144
|
+
Group time-based metrics into buckets:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class DailyMetric < Familia::Horreum
|
148
|
+
feature :quantization
|
149
|
+
string :counter, default_expiration: 1.day, quantize: [10.minutes, '%H:%M']
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
This automatically groups metrics into 10-minute intervals formatted as "HH:MM", ideal for analytics dashboards.
|
154
|
+
|
155
|
+
## Advanced Patterns
|
156
|
+
|
157
|
+
### Custom Methods and Logic
|
158
|
+
|
159
|
+
Add domain-specific behavior to your models:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
class User < Familia::Horreum
|
163
|
+
field :first_name
|
164
|
+
field :last_name
|
165
|
+
field :status
|
166
|
+
|
167
|
+
def full_name
|
168
|
+
"#{first_name} #{last_name}"
|
169
|
+
end
|
170
|
+
|
171
|
+
def active?
|
172
|
+
status == 'active'
|
173
|
+
end
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
These methods work alongside Familia's persistence layer, letting you build rich domain models.
|
178
|
+
|
179
|
+
### Transactional Operations
|
180
|
+
|
181
|
+
Execute multiple Valkey commands atomically:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
user.transaction do |conn|
|
185
|
+
conn.set("user:#{user.id}:status", "active")
|
186
|
+
conn.zadd("active_users", Time.now.to_i, user.id)
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
Preserves data integrity for complex operations that require multiple Valkey commands.
|
191
|
+
|
192
|
+
### Connection Management and Pooling
|
193
|
+
|
194
|
+
Configure connection pooling for production environments:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
require 'connection_pool'
|
198
|
+
|
199
|
+
pools = {
|
200
|
+
"redis://localhost:6379/0" => ConnectionPool.new(size: 10) { Redis.new(db: 0) }
|
201
|
+
}
|
202
|
+
|
203
|
+
Familia.connection_provider = lambda do |uri|
|
204
|
+
pool = pools[uri]
|
205
|
+
pool.with { |conn| conn }
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
209
|
+
This ensures efficient Valkey connection usage in multi-threaded applications.
|
210
|
+
|
211
|
+
### Encrypted Fields
|
212
|
+
|
213
|
+
Protect sensitive data at rest:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
class SecureUser < Familia::Horreum
|
217
|
+
feature :encrypted_fields
|
218
|
+
|
219
|
+
identifier_field :id
|
220
|
+
field :id
|
221
|
+
field :email # Plain text
|
222
|
+
encrypted_field :ssn # Encrypted
|
223
|
+
encrypted_field :notes, aad_fields: [:id, :email] # With auth data
|
224
|
+
end
|
225
|
+
```
|
226
|
+
|
227
|
+
Uses authenticated encryption to protect data while allowing selective field access.
|
228
|
+
|
229
|
+
### Open-ended Serialization
|
230
|
+
|
231
|
+
Customize how objects are serialized to Valkey:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
class JsonModel < Familia::Horreum
|
235
|
+
def serialize_value
|
236
|
+
JSON.generate(to_h)
|
237
|
+
end
|
238
|
+
|
239
|
+
def self.deserialize_value(data)
|
240
|
+
new(**JSON.parse(data, symbolize_names: true))
|
241
|
+
end
|
242
|
+
end
|
243
|
+
```
|
244
|
+
|
245
|
+
Enables integration with custom serialization formats beyond Familia's defaults.
|
246
|
+
|
247
|
+
## Configuration
|
248
|
+
|
249
|
+
### Basic Setup
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
# Simple connection
|
253
|
+
Familia.uri = 'redis://localhost:6379/0'
|
254
|
+
|
255
|
+
# Multiple databases
|
256
|
+
Familia.redis_config = {
|
257
|
+
host: 'localhost',
|
258
|
+
port: 6379,
|
259
|
+
db: 0,
|
260
|
+
timeout: 5
|
261
|
+
}
|
262
|
+
```
|
263
|
+
|
264
|
+
### Encryption Setup (Optional)
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
# Generate base64-encoded 32-byte keys
|
268
|
+
Familia.config.encryption_keys = {
|
269
|
+
v1: Base64.strict_encode64(SecureRandom.bytes(32)),
|
270
|
+
v2: Base64.strict_encode64(SecureRandom.bytes(32))
|
271
|
+
}
|
272
|
+
Familia.config.current_key_version = :v2
|
273
|
+
```
|
274
|
+
|
275
|
+
## Common Patterns
|
276
|
+
|
277
|
+
### Bulk Operations
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
# Load multiple objects
|
281
|
+
users = User.multiget('alice@example.com', 'bob@example.com')
|
282
|
+
|
283
|
+
# Batch operations
|
284
|
+
User.transaction do |conn|
|
285
|
+
conn.set('user:alice:status', 'active')
|
286
|
+
conn.zadd('active_users', Time.now.to_i, 'alice')
|
287
|
+
end
|
288
|
+
```
|
289
|
+
|
290
|
+
### Error Handling
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
begin
|
294
|
+
user = User.load('nonexistent@example.com')
|
295
|
+
rescue Familia::Problem => e
|
296
|
+
puts "User not found: #{e.message}"
|
297
|
+
end
|
298
|
+
|
299
|
+
# Safe loading
|
300
|
+
user = User.load('maybe@example.com') || User.new
|
301
|
+
```
|
302
|
+
|
303
|
+
## Troubleshooting
|
304
|
+
|
305
|
+
### Common Issues
|
306
|
+
|
307
|
+
**Connection Errors:**
|
308
|
+
```ruby
|
309
|
+
# Check connection
|
310
|
+
Familia.connect_to_uri('redis://localhost:6379/0')
|
311
|
+
```
|
312
|
+
|
313
|
+
**Missing Keys:**
|
314
|
+
```ruby
|
315
|
+
# Debug key names
|
316
|
+
user = User.new(email: 'test@example.com')
|
317
|
+
puts user.rediskey # Shows the Valkey key that would be used
|
318
|
+
```
|
319
|
+
|
320
|
+
**Encryption Issues:**
|
321
|
+
```ruby
|
322
|
+
# Validate encryption config
|
323
|
+
Familia::Encryption.validate_configuration!
|
324
|
+
```
|
325
|
+
|
326
|
+
### Debug Mode
|
327
|
+
|
328
|
+
```ruby
|
329
|
+
# Enable debug logging
|
330
|
+
Familia.debug = true
|
331
|
+
|
332
|
+
# Check what's in Valkey
|
333
|
+
Familia.redis.keys('*') # List all keys (use carefully in production)
|
334
|
+
```
|
335
|
+
|
336
|
+
## Testing
|
337
|
+
|
338
|
+
### Test Configuration
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
# test_helper.rb or spec_helper.rb
|
342
|
+
require 'familia'
|
343
|
+
|
344
|
+
# Use separate test database
|
345
|
+
Familia.uri = 'redis://localhost:6379/15'
|
346
|
+
|
347
|
+
# Setup encryption for tests
|
348
|
+
test_keys = {
|
349
|
+
v1: Base64.strict_encode64('a' * 32),
|
350
|
+
v2: Base64.strict_encode64('b' * 32)
|
351
|
+
}
|
352
|
+
Familia.config.encryption_keys = test_keys
|
353
|
+
Familia.config.current_key_version = :v1
|
354
|
+
|
355
|
+
# Clear data between tests
|
356
|
+
def clear_redis
|
357
|
+
Familia.redis.flushdb
|
358
|
+
end
|
359
|
+
```
|
@@ -0,0 +1,270 @@
|
|
1
|
+
# API Reference
|
2
|
+
|
3
|
+
## Class Methods
|
4
|
+
|
5
|
+
### encrypted_field
|
6
|
+
|
7
|
+
Defines an encrypted field on a Familia::Horreum class.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
encrypted_field(name, **options)
|
11
|
+
```
|
12
|
+
|
13
|
+
**Parameters:**
|
14
|
+
- `name` (Symbol) - Field name
|
15
|
+
- `**options` (Hash) - Standard field options plus encryption-specific options
|
16
|
+
|
17
|
+
**Options:**
|
18
|
+
- `:as` - Custom accessor method name
|
19
|
+
- `:on_conflict` - Conflict resolution (always `:raise` for encrypted fields)
|
20
|
+
|
21
|
+
**Example:**
|
22
|
+
```ruby
|
23
|
+
class User < Familia::Horreum
|
24
|
+
encrypted_field :favorite_snack
|
25
|
+
encrypted_field :api_key, as: :secret_key
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
### encrypted_fields
|
30
|
+
|
31
|
+
Returns list of encrypted field names.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
User.encrypted_fields # => [:favorite_snack, :api_key]
|
35
|
+
```
|
36
|
+
|
37
|
+
## Instance Methods
|
38
|
+
|
39
|
+
### Field Accessors
|
40
|
+
|
41
|
+
Encrypted fields provide standard accessors:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
user.favorite_snack # Get decrypted value
|
45
|
+
user.favorite_snack = value # Set and encrypt value
|
46
|
+
user.favorite_snack! # Fast write (still encrypted)
|
47
|
+
```
|
48
|
+
|
49
|
+
### Passphrase-Protected Access
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# For passphrase-protected fields
|
53
|
+
vault.secret_data(passphrase_value: "user_passphrase")
|
54
|
+
```
|
55
|
+
|
56
|
+
## Familia::Encryption Module
|
57
|
+
|
58
|
+
### encrypt
|
59
|
+
|
60
|
+
Encrypts plaintext with context-specific key.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
Familia::Encryption.encrypt(plaintext,
|
64
|
+
context: "User:favorite_snack:user123",
|
65
|
+
additional_data: nil
|
66
|
+
)
|
67
|
+
```
|
68
|
+
|
69
|
+
**Parameters:**
|
70
|
+
- `plaintext` (String) - Data to encrypt
|
71
|
+
- `context` (String) - Key derivation context
|
72
|
+
- `additional_data` (String, nil) - Optional AAD for authentication
|
73
|
+
|
74
|
+
**Returns:** JSON string with encrypted data structure
|
75
|
+
|
76
|
+
### decrypt
|
77
|
+
|
78
|
+
Decrypts ciphertext with context-specific key.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
Familia::Encryption.decrypt(encrypted_json,
|
82
|
+
context: "User:favorite_snack:user123",
|
83
|
+
additional_data: nil
|
84
|
+
)
|
85
|
+
```
|
86
|
+
|
87
|
+
**Parameters:**
|
88
|
+
- `encrypted_json` (String) - JSON-encoded encrypted data
|
89
|
+
- `context` (String) - Key derivation context
|
90
|
+
- `additional_data` (String, nil) - Optional AAD for verification
|
91
|
+
|
92
|
+
**Returns:** Decrypted plaintext string
|
93
|
+
|
94
|
+
### validate_configuration!
|
95
|
+
|
96
|
+
Validates encryption configuration at startup.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
Familia::Encryption.validate_configuration!
|
100
|
+
# Raises Familia::EncryptionError if configuration invalid
|
101
|
+
```
|
102
|
+
|
103
|
+
### with_key_cache
|
104
|
+
|
105
|
+
Provides request-scoped key caching.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
Familia::Encryption.with_key_cache do
|
109
|
+
# Operations here share derived keys
|
110
|
+
users.each { |u| u.decrypt_fields }
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
## Configuration
|
115
|
+
|
116
|
+
### Familia.configure
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
Familia.configure do |config|
|
120
|
+
# Single key configuration
|
121
|
+
config.encryption_keys = {
|
122
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY']
|
123
|
+
}
|
124
|
+
config.current_key_version = :v1
|
125
|
+
|
126
|
+
# Multi-version configuration
|
127
|
+
config.encryption_keys = {
|
128
|
+
v1_2024: ENV['OLD_KEY'],
|
129
|
+
v2_2025: ENV['NEW_KEY']
|
130
|
+
}
|
131
|
+
config.current_key_version = :v2_2025
|
132
|
+
|
133
|
+
# Key cache TTL (seconds)
|
134
|
+
config.key_cache_ttl = 300 # Default: 5 minutes
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
## Data Types
|
139
|
+
|
140
|
+
### EncryptedData
|
141
|
+
|
142
|
+
Internal data structure for encrypted values.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
EncryptedData = Data.define(
|
146
|
+
:library, # "libsodium" or "openssl"
|
147
|
+
:algorithm, # "xchacha20poly1305" or "aes-256-gcm"
|
148
|
+
:nonce, # Base64-encoded nonce/IV
|
149
|
+
:ciphertext, # Base64-encoded ciphertext
|
150
|
+
:key_version # Key version identifier
|
151
|
+
)
|
152
|
+
```
|
153
|
+
|
154
|
+
### RedactedString
|
155
|
+
|
156
|
+
String subclass that redacts sensitive data in output.
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class RedactedString < String
|
160
|
+
def to_s
|
161
|
+
'[REDACTED]'
|
162
|
+
end
|
163
|
+
|
164
|
+
def inspect
|
165
|
+
'[REDACTED]'
|
166
|
+
end
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
## Exceptions
|
171
|
+
|
172
|
+
### Familia::EncryptionError
|
173
|
+
|
174
|
+
Raised for encryption/decryption failures.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
begin
|
178
|
+
user.decrypt_field(:favorite_snack)
|
179
|
+
rescue Familia::EncryptionError => e
|
180
|
+
case e.message
|
181
|
+
when /key version/
|
182
|
+
# Handle key version mismatch
|
183
|
+
when /authentication/
|
184
|
+
# Handle tampering
|
185
|
+
else
|
186
|
+
# Handle other errors
|
187
|
+
end
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
## CLI Commands
|
192
|
+
|
193
|
+
### Generate Key
|
194
|
+
|
195
|
+
```bash
|
196
|
+
$ familia encryption:generate_key [--bits 256]
|
197
|
+
# Outputs Base64-encoded key
|
198
|
+
```
|
199
|
+
|
200
|
+
### Verify Encryption
|
201
|
+
|
202
|
+
```bash
|
203
|
+
$ familia encryption:verify [--model User] [--field favorite_snack]
|
204
|
+
# Verifies field encryption is working
|
205
|
+
```
|
206
|
+
|
207
|
+
### Rotate Keys
|
208
|
+
|
209
|
+
```bash
|
210
|
+
$ familia encryption:rotate [--from v1] [--to v2]
|
211
|
+
# Migrates encrypted fields to new key
|
212
|
+
```
|
213
|
+
|
214
|
+
## Testing Helpers
|
215
|
+
|
216
|
+
### EncryptionTestHelpers
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
module Familia::EncryptionTestHelpers
|
220
|
+
# Set up test encryption keys
|
221
|
+
def with_test_encryption_keys(&block)
|
222
|
+
|
223
|
+
# Verify field is encrypted in storage
|
224
|
+
def assert_field_encrypted(model, field)
|
225
|
+
|
226
|
+
# Verify decryption works
|
227
|
+
def assert_decryption_works(model, field, expected)
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
### RSpec Example
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
RSpec.describe User do
|
235
|
+
include Familia::EncryptionTestHelpers
|
236
|
+
|
237
|
+
it "encrypts favorite snack field" do
|
238
|
+
with_test_encryption_keys do
|
239
|
+
user = User.create(favorite_snack: "chocolate chip cookies")
|
240
|
+
|
241
|
+
assert_field_encrypted(user, :favorite_snack)
|
242
|
+
assert_decryption_works(user, :favorite_snack, "chocolate chip cookies")
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
```
|
247
|
+
|
248
|
+
## Performance Considerations
|
249
|
+
|
250
|
+
### Key Derivation Caching
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
# Automatic in web requests
|
254
|
+
class ApplicationController
|
255
|
+
around_action :with_encryption_cache
|
256
|
+
|
257
|
+
def with_encryption_cache
|
258
|
+
Familia::Encryption.with_key_cache { yield }
|
259
|
+
end
|
260
|
+
end
|
261
|
+
```
|
262
|
+
|
263
|
+
### Batch Operations
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
# Efficient for bulk operations
|
267
|
+
User.batch_decrypt(:favorite_snack) do |users|
|
268
|
+
users.each { |u| process(u.favorite_snack) }
|
269
|
+
end
|
270
|
+
```
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Encrypted Fields Overview
|
2
|
+
|
3
|
+
## Quick Start
|
4
|
+
|
5
|
+
Add encrypted field support to any Familia model in one line:
|
6
|
+
class User < Familia::Horreum
|
7
|
+
encrypted_field :diary_entry
|
8
|
+
end
|
9
|
+
```
|
10
|
+
|
11
|
+
## What It Does
|
12
|
+
|
13
|
+
- **Automatic Encryption**: Fields are encrypted before storing in Redis/Valkey
|
14
|
+
- **Transparent Decryption**: Access encrypted fields like normal attributes
|
15
|
+
- **Secure by Default**: Uses authenticated encryption (AES-GCM or XChaCha20-Poly1305)
|
16
|
+
- **Zero Boilerplate**: No manual encrypt/decrypt calls needed
|
17
|
+
|
18
|
+
## When to Use
|
19
|
+
|
20
|
+
Use encrypted fields for:
|
21
|
+
- Personal Identifiable Information (PII)
|
22
|
+
- API keys and secrets
|
23
|
+
- Medical records
|
24
|
+
- Financial data
|
25
|
+
- Any sensitive user data
|
26
|
+
|
27
|
+
## Basic Example
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class Customer < Familia::Horreum
|
31
|
+
field :email # Regular field
|
32
|
+
encrypted_field :secret_recipe # Encrypted field
|
33
|
+
encrypted_field :diary_entry # Another encrypted field
|
34
|
+
end
|
35
|
+
|
36
|
+
# Usage is identical to regular fields
|
37
|
+
customer = Customer.new(
|
38
|
+
email: 'user@example.com',
|
39
|
+
secret_recipe: 'Add extra vanilla',
|
40
|
+
diary_entry: 'Today I learned Redis is fast'
|
41
|
+
)
|
42
|
+
|
43
|
+
customer.save
|
44
|
+
customer.credit_card # => "4111-1111-1111-1111" (decrypted automatically)
|
45
|
+
```
|
46
|
+
|
47
|
+
## Configuration
|
48
|
+
|
49
|
+
Set your encryption key in environment:
|
50
|
+
|
51
|
+
```bash
|
52
|
+
export FAMILIA_ENCRYPTION_KEY=$(familia encryption:generate_key)
|
53
|
+
```
|
54
|
+
|
55
|
+
Configure in your app:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
Familia.configure do |config|
|
59
|
+
config.encryption_keys = {
|
60
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY']
|
61
|
+
}
|
62
|
+
config.current_key_version = :v1
|
63
|
+
end
|
64
|
+
```
|