Skip to content

Commit 348f806

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 60518e5 commit 348f806

9 files changed

Lines changed: 259 additions & 203 deletions

File tree

docker/tinode/config.template

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,6 @@
7272
"uid_key": "$UID_ENCRYPTION_KEY",
7373
"max_results": 1024,
7474
"use_adapter": "$STORE_USE_ADAPTER",
75-
"encryption": {
76-
"enabled": false,
77-
"key": ""
78-
},
7975
"adapters": {
8076
"mysql": {
8177
"database": "tinode",

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: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,6 @@ var globals struct {
214214

215215
// Maximum age of messages which can be deleted with 'D' permission.
216216
msgDeleteAge time.Duration
217-
218-
// Message encryption settings
219-
encryptionEnabled bool
220-
encryptionKey string
221217
}
222218

223219
// Credential validator config.
@@ -353,9 +349,6 @@ func main() {
353349
"Override the URL path where the server's internal status is displayed. Use '-' to disable.")
354350
pprofFile := flag.String("pprof", "", "File name to save profiling info to. Disabled if not set.")
355351
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)")
359352

360353
flag.Parse()
361354

@@ -399,10 +392,6 @@ func main() {
399392
config.Listen = *listenOn
400393
}
401394

402-
// Store encryption flags for later use in store initialization
403-
globals.encryptionEnabled = *encryptionEnabled
404-
globals.encryptionKey = *encryptionKey
405-
406395
// Set up HTTP server. Must use non-default mux because of expvar.
407396
mux := http.NewServeMux()
408397

@@ -448,11 +437,6 @@ func main() {
448437
logs.Info.Printf("Profiling info saved to '%s.(cpu|mem)'", *pprofFile)
449438
}
450439

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-
456440
err = store.Store.Open(workerId, config.Store)
457441
logs.Info.Println("DB adapter", store.Store.GetAdapterName(), store.Store.GetAdapterVersion())
458442
if err != nil {

server/store/ENCRYPTION.md

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
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+
11+
- AES-128: 16 bytes (128 bits)
12+
- AES-192: 24 bytes (192 bits)
13+
- AES-256: 32 bytes (256 bits)
814

915
## Configuration
1016

@@ -15,44 +21,56 @@ Add encryption settings to your `tinode.conf` file:
1521
```json
1622
{
1723
"store_config": {
18-
"encryption": {
19-
"enabled": true,
20-
"key": "base64-encoded-32-byte-key-here"
24+
"encrypt_at_rest": {
25+
"key": "base64-encoded-key-here"
2126
}
2227
}
2328
}
2429
```
2530

31+
**Note:** If no key is provided or the key is empty, encryption is disabled.
32+
2633
### Command Line Flags
2734

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

3037
```bash
31-
./tinode-server --encryption_enabled --encryption_key "base64-encoded-32-byte-key-here"
38+
./tinode-server --message_encrypt_at_rest_key "base64-encoded-key-here"
3239
```
3340

3441
## Key Management
3542

3643
### Generating an Encryption Key
3744

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

4047
```bash
41-
# Generate 32-byte encryption key
48+
# Generate 32-byte encryption key (AES-256)
4249
cd keygen
4350
./keygen -encryption
4451

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

48-
# Save key to file
49-
./keygen -encryption -output encryption.key
55+
# Generate 24-byte encryption key (AES-192)
56+
./keygen -encryption -keysize 24
57+
58+
# Save key to file using shell redirection
59+
./keygen -encryption > encryption.key
5060
```
5161

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

5466
```bash
55-
# Generate 32 random bytes and encode in base64
67+
# Generate 16 random bytes and encode in base64 (AES-128)
68+
openssl rand -base64 16
69+
70+
# Generate 24 random bytes and encode in base64 (AES-192)
71+
openssl rand -base64 24
72+
73+
# Generate 32 random bytes and encode in base64 (AES-256)
5674
openssl rand -base64 32
5775
```
5876

@@ -84,6 +102,8 @@ go run server/tools/encrypt_messages.go \
84102
--topic "your-topic-name"
85103
```
86104

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

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

122142
### Encryption Algorithm
123143

124-
- **Cipher**: AES-256
144+
- **Cipher**: AES (Advanced Encryption Standard)
125145
- **Mode**: GCM (Galois/Counter Mode)
126-
- **Key size**: 256 bits (32 bytes)
146+
- **Key sizes**: 128, 192, or 256 bits (16, 24, or 32 bytes)
127147
- **Nonce**: Random 12-byte nonce for each message
128148

129149
### Storage Format
130150

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

133153
```json
134154
{
135155
"data": "base64-encoded-encrypted-data",
136-
"nonce": "base64-encoded-nonce",
156+
"nonce": "base64-encoded-nonce",
137157
"encrypted": true
138158
}
139159
```
140160

161+
The `data` and `nonce` fields are automatically base64 encoded/decoded during JSON marshaling/unmarshaling.
162+
141163
### Performance Impact
142164

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

149171
### Common Issues
150172

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

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

158181
3. **"failed to create AES cipher"**
159182
- This usually indicates a system-level issue with crypto libraries
@@ -172,6 +195,31 @@ Encryption-related errors and warnings are logged with the prefix:
172195
- Per-topic encryption settings
173196
- End-to-end encryption support
174197

198+
## Migration from Previous Versions
199+
200+
**New configuration:**
201+
202+
```json
203+
{
204+
"store_config": {
205+
"encrypt_at_rest": {
206+
"key": "base64-encoded-key"
207+
}
208+
}
209+
}
210+
```
211+
212+
**Key changes:**
213+
- Support for multiple AES key sizes (16, 24, 32 bytes) instead of just 32 bytes
214+
- Command line flag renamed from `--encryption_key` to `--message_encrypt_at_rest_key`
215+
216+
### Migration Steps
217+
218+
1. **Update your configuration file** to use the new field names
219+
2. **Test with a dry run** using the migration tool
220+
3. **Restart the server** with the new configuration
221+
4. **Verify encryption is working** by checking logs and database content
222+
175223
## API Changes
176224

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

0 commit comments

Comments
 (0)