Skip to content

Commit 157476a

Browse files
committed
#967: improve message encryption at rest with better naming and validation
- Rename generic 'encryption' config to 'encrypt_at_rest' for clarity - Remove redundant 'enabled' field - key presence determines encryption - Support all AES key sizes (16, 24, 32 bytes) instead of just 32-byte keys - Simplify EncryptionService to MessageEncryptionService with cleaner API - Use []byte fields in EncryptedContent for automatic base64 conversion - Fix store initialization order: command line flags override config file - Update keygen tool with proper AES key size validation - Remove output file option from keygen (use shell redirection instead) - Fix encrypt_messages tool to use proper store interface methods - Add nil content handling in EncryptContent method - Update all tests to work with new MessageEncryptionService API - Improve error handling and method visibility throughout [#967]
1 parent 0b92da9 commit 157476a

File tree

9 files changed

+260
-205
lines changed

9 files changed

+260
-205
lines changed

docker/tinode/config.template

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@
7272
"uid_key": "$UID_ENCRYPTION_KEY",
7373
"max_results": 1024,
7474
"use_adapter": "$STORE_USE_ADAPTER",
75-
"encryption": {
76-
"enabled": false,
75+
"encrypt_at_rest": {
7776
"key": ""
7877
},
7978
"adapters": {

keygen/README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ A command-line utility to generate an API key for [Tinode server](../server/)
1212
**Encryption Key Generation:**
1313

1414
* `encryption`: Generate encryption key instead of API key.
15-
* `keysize`: Encryption key size in bytes (default: 32 for AES-256).
16-
* `output`: Output file for the encryption key (optional).
15+
* `keysize`: Encryption key size in bytes. Must be 16 (AES-128), 24 (AES-192), or 32 (AES-256). Default: 32.
1716

1817

1918
## Usage
@@ -39,14 +38,17 @@ HMAC salt: TC0Jzr8f28kAspXrb4UYccJUJ63b7CSA16n1qMxxGpw=
3938
**Generate Encryption Key:**
4039

4140
```sh
42-
# Generate 32-byte encryption key
41+
# Generate 32-byte encryption key (AES-256)
4342
./keygen -encryption
4443

45-
# Generate custom size key
46-
./keygen -encryption -keysize 32
44+
# Generate 16-byte encryption key (AES-128)
45+
./keygen -encryption -keysize 16
4746

48-
# Save key to file
49-
./keygen -encryption -output encryption.key
47+
# Generate 24-byte encryption key (AES-192)
48+
./keygen -encryption -keysize 24
49+
50+
# Save key to file using shell redirection
51+
./keygen -encryption > encryption.key
5052
```
5153

5254
Sample encryption key output:
@@ -56,8 +58,7 @@ Generated 32-byte encryption key:
5658
dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=
5759
5860
Add this to your tinode.conf:
59-
"encryption": {
60-
"enabled": true,
61+
"encrypt_at_rest": {
6162
"key": "dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q="
6263
}
6364
```

keygen/keygen.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,12 @@ func main() {
2828

2929
// Encryption key generation flags
3030
encryptionKey := flag.Bool("encryption", false, "Generate encryption key instead of API key")
31-
keySize := flag.Int("keysize", 32, "Encryption key size in bytes (default: 32 for AES-256)")
32-
outputFile := flag.String("output", "", "Output file for the encryption key (optional)")
31+
keySize := flag.Int("keysize", 32, "Encryption key size in bytes (16, 24, or 32 for AES-128/192/256)")
3332

3433
flag.Parse()
3534

3635
if *encryptionKey {
37-
os.Exit(generateEncryptionKey(*keySize, *outputFile))
36+
os.Exit(generateEncryptionKey(*keySize))
3837
} else if *apikey != "" {
3938
if *hmacSalt == "" {
4039
log.Println("Error: must provide HMAC salt for key validation")
@@ -181,7 +180,13 @@ func validate(apikey string, hmacSaltB64 string) int {
181180
}
182181

183182
// generateEncryptionKey generates a random encryption key of specified size
184-
func generateEncryptionKey(keySize int, outputFile string) int {
183+
func generateEncryptionKey(keySize int) int {
184+
// Validate key size - AES supports 16, 24, or 32 bytes
185+
if keySize != 16 && keySize != 24 && keySize != 32 {
186+
log.Printf("Error: Invalid key size %d. Must be 16 (AES-128), 24 (AES-192), or 32 (AES-256) bytes", keySize)
187+
return 1
188+
}
189+
185190
// Generate random key
186191
key := make([]byte, keySize)
187192
if _, err := rand.Read(key); err != nil {
@@ -193,21 +198,12 @@ func generateEncryptionKey(keySize int, outputFile string) int {
193198
encodedKey := base64.StdEncoding.EncodeToString(key)
194199

195200
// Output
196-
if outputFile != "" {
197-
if err := os.WriteFile(outputFile, []byte(encodedKey), 0600); err != nil {
198-
log.Println("Error: Failed to write key to file", err)
199-
return 1
200-
}
201-
fmt.Printf("Encryption key written to %s\n", outputFile)
202-
} else {
203-
fmt.Printf("Generated %d-byte encryption key:\n", keySize)
204-
fmt.Printf("%s\n", encodedKey)
205-
fmt.Printf("\nAdd this to your tinode.conf:\n")
206-
fmt.Printf(`"encryption": {
207-
"enabled": true,
201+
fmt.Printf("Generated %d-byte encryption key:\n", keySize)
202+
fmt.Printf("%s\n", encodedKey)
203+
fmt.Printf("\nAdd this to your tinode.conf:\n")
204+
fmt.Printf(`"encrypt_at_rest": {
208205
"key": "%s"
209206
}`, encodedKey)
210-
}
211207

212208
return 0
213209
}

server/main.go

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,8 @@ var globals struct {
215215
// Maximum age of messages which can be deleted with 'D' permission.
216216
msgDeleteAge time.Duration
217217

218-
// Message encryption settings
219-
encryptionEnabled bool
220-
encryptionKey string
218+
// Message encryption at rest settings
219+
messageEncryptAtRestKey string
221220
}
222221

223222
// Credential validator config.
@@ -353,9 +352,8 @@ func main() {
353352
"Override the URL path where the server's internal status is displayed. Use '-' to disable.")
354353
pprofFile := flag.String("pprof", "", "File name to save profiling info to. Disabled if not set.")
355354
pprofUrl := flag.String("pprof_url", "", "Debugging only! URL path for exposing profiling info. Disabled if not set.")
356-
// Encryption flags
357-
encryptionEnabled := flag.Bool("encryption_enabled", false, "Enable message encryption")
358-
encryptionKey := flag.String("encryption_key", "", "32-byte encryption key (base64 encoded)")
355+
// Message encryption at rest flag
356+
messageEncryptAtRestKey := flag.String("message_encrypt_at_rest_key", "", "AES encryption key for message content (16, 24, or 32 bytes, base64 encoded)")
359357

360358
flag.Parse()
361359

@@ -399,9 +397,8 @@ func main() {
399397
config.Listen = *listenOn
400398
}
401399

402-
// Store encryption flags for later use in store initialization
403-
globals.encryptionEnabled = *encryptionEnabled
404-
globals.encryptionKey = *encryptionKey
400+
// Store message encryption at rest key for later use in store initialization
401+
globals.messageEncryptAtRestKey = *messageEncryptAtRestKey
405402

406403
// Set up HTTP server. Must use non-default mux because of expvar.
407404
mux := http.NewServeMux()
@@ -448,12 +445,7 @@ func main() {
448445
logs.Info.Printf("Profiling info saved to '%s.(cpu|mem)'", *pprofFile)
449446
}
450447

451-
// Initialize encryption service from command line flags
452-
if err := store.InitEncryptionFromFlags(globals.encryptionEnabled, globals.encryptionKey); err != nil {
453-
logs.Err.Fatal("Failed to initialize encryption: ", err)
454-
}
455-
456-
err = store.Store.Open(workerId, config.Store)
448+
err = store.Store.Open(workerId, globals.messageEncryptAtRestKey, config.Store)
457449
logs.Info.Println("DB adapter", store.Store.GetAdapterName(), store.Store.GetAdapterVersion())
458450
if err != nil {
459451
logs.Err.Fatal("Failed to connect to DB: ", err)

server/store/ENCRYPTION.md

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
# Message Encryption
1+
# Message Encryption at Rest
22

3-
This document describes the message encryption feature in Tinode, which allows encrypting message content stored in the database to prevent unauthorized access to message content using database tools.
3+
This document describes the message encryption at rest feature in Tinode, which allows encrypting message content stored in the database to prevent unauthorized access to message content using database tools.
44

55
## Overview
66

7-
The encryption feature uses AES-256-GCM symmetric encryption to encrypt only the `content` field of messages. The encryption is transparent to clients - messages are automatically encrypted when saved and decrypted when retrieved.
7+
The encryption feature uses AES-GCM symmetric encryption to encrypt only the `content` field of messages. The encryption is transparent to clients - messages are automatically encrypted when saved and decrypted when retrieved.
8+
9+
**Supported AES key sizes:**
10+
- AES-128: 16 bytes (128 bits)
11+
- AES-192: 24 bytes (192 bits)
12+
- AES-256: 32 bytes (256 bits)
813

914
## Configuration
1015

@@ -15,44 +20,56 @@ Add encryption settings to your `tinode.conf` file:
1520
```json
1621
{
1722
"store_config": {
18-
"encryption": {
19-
"enabled": true,
20-
"key": "base64-encoded-32-byte-key-here"
23+
"encrypt_at_rest": {
24+
"key": "base64-encoded-key-here"
2125
}
2226
}
2327
}
2428
```
2529

30+
**Note:** If no key is provided or the key is empty, encryption is disabled.
31+
2632
### Command Line Flags
2733

28-
You can also enable encryption via command line flags:
34+
You can also enable encryption via command line flags (overrides config file):
2935

3036
```bash
31-
./tinode-server --encryption_enabled --encryption_key "base64-encoded-32-byte-key-here"
37+
./tinode-server --message_encrypt_at_rest_key "base64-encoded-key-here"
3238
```
3339

3440
## Key Management
3541

3642
### Generating an Encryption Key
3743

38-
Generate a 32-byte (256-bit) random key using the built-in keygen tool:
44+
Generate a random key using the built-in keygen tool:
3945

4046
```bash
41-
# Generate 32-byte encryption key
47+
# Generate 32-byte encryption key (AES-256)
4248
cd keygen
4349
./keygen -encryption
4450

45-
# Generate custom size key
46-
./keygen -encryption -keysize 32
51+
# Generate 16-byte encryption key (AES-128)
52+
./keygen -encryption -keysize 16
53+
54+
# Generate 24-byte encryption key (AES-192)
55+
./keygen -encryption -keysize 24
4756

48-
# Save key to file
49-
./keygen -encryption -output encryption.key
57+
# Save key to file using shell redirection
58+
./keygen -encryption > encryption.key
5059
```
5160

61+
The keygen tool validates that the key size is exactly 16, 24, or 32 bytes.
62+
5263
Alternatively, you can use OpenSSL:
5364

5465
```bash
55-
# Generate 32 random bytes and encode in base64
66+
# Generate 16 random bytes and encode in base64 (AES-128)
67+
openssl rand -base64 16
68+
69+
# Generate 24 random bytes and encode in base64 (AES-192)
70+
openssl rand -base64 24
71+
72+
# Generate 32 random bytes and encode in base64 (AES-256)
5673
openssl rand -base64 32
5774
```
5875

@@ -84,6 +101,8 @@ go run server/tools/encrypt_messages.go \
84101
--topic "your-topic-name"
85102
```
86103

104+
**Note:** The migration tool now uses the proper store interface and handles all supported AES key sizes.
105+
87106
### From Encrypted to Unencrypted
88107

89108
To decrypt encrypted messages (use with caution):
@@ -121,23 +140,25 @@ go run server/tools/encrypt_messages.go \
121140

122141
### Encryption Algorithm
123142

124-
- **Cipher**: AES-256
143+
- **Cipher**: AES (Advanced Encryption Standard)
125144
- **Mode**: GCM (Galois/Counter Mode)
126-
- **Key size**: 256 bits (32 bytes)
145+
- **Key sizes**: 128, 192, or 256 bits (16, 24, or 32 bytes)
127146
- **Nonce**: Random 12-byte nonce for each message
128147

129148
### Storage Format
130149

131-
Encrypted content is stored as a JSON object:
150+
Encrypted content is stored as a JSON object with automatic base64 encoding:
132151

133152
```json
134153
{
135154
"data": "base64-encoded-encrypted-data",
136-
"nonce": "base64-encoded-nonce",
155+
"nonce": "base64-encoded-nonce",
137156
"encrypted": true
138157
}
139158
```
140159

160+
The `data` and `nonce` fields are automatically base64 encoded/decoded during JSON marshaling/unmarshaling.
161+
141162
### Performance Impact
142163

143164
- **Encryption**: ~1-5ms per message (depending on content size)
@@ -148,12 +169,13 @@ Encrypted content is stored as a JSON object:
148169

149170
### Common Issues
150171

151-
1. **"encryption key is required when encryption is enabled"**
152-
- Ensure you've provided a valid base64-encoded 32-byte key
172+
1. **"encryption key must be 16, 24, or 32 bytes"**
173+
- Ensure your key is exactly 16, 24, or 32 bytes when decoded from base64
174+
- Use the keygen tool to generate valid keys
153175

154176
2. **"failed to decode base64 encryption key"**
155177
- Verify your key is properly base64-encoded
156-
- Ensure the key is exactly 32 bytes when decoded
178+
- Ensure there are no extra spaces or newlines in the key
157179

158180
3. **"failed to create AES cipher"**
159181
- This usually indicates a system-level issue with crypto libraries
@@ -172,6 +194,48 @@ Encryption-related errors and warnings are logged with the prefix:
172194
- Per-topic encryption settings
173195
- End-to-end encryption support
174196

197+
## Migration from Previous Versions
198+
199+
### Breaking Changes
200+
201+
If you're upgrading from a previous version with encryption enabled, you'll need to update your configuration:
202+
203+
**Old configuration:**
204+
```json
205+
{
206+
"store_config": {
207+
"encryption": {
208+
"enabled": true,
209+
"key": "base64-encoded-key"
210+
}
211+
}
212+
}
213+
```
214+
215+
**New configuration:**
216+
```json
217+
{
218+
"store_config": {
219+
"encrypt_at_rest": {
220+
"key": "base64-encoded-key"
221+
}
222+
}
223+
}
224+
```
225+
226+
**Key changes:**
227+
- Configuration field renamed from `encryption` to `encrypt_at_rest`
228+
- Removed `enabled` field - empty key means encryption is disabled
229+
- Support for multiple AES key sizes (16, 24, 32 bytes) instead of just 32 bytes
230+
- Command line flag renamed from `--encryption_key` to `--message_encrypt_at_rest_key`
231+
232+
### Migration Steps
233+
234+
1. **Update your configuration file** to use the new field names
235+
2. **Test with a dry run** using the migration tool
236+
3. **Restart the server** with the new configuration
237+
4. **Verify encryption is working** by checking logs and database content
238+
175239
## API Changes
176240

177241
No changes to the client API are required. Messages are automatically encrypted/decrypted transparently.

0 commit comments

Comments
 (0)