Skip to content

Commit 60518e5

Browse files
committed
#967: feature/encrypt message at rest
Key Features - Optional: Encryption can be disabled if not needed - Transparent: No changes to client API required - Secure: Uses AES-256-GCM with random nonces - Configurable: Via config file or command line - Migration Support: Tools to convert existing installations - Performance Optimized: Only encrypts message content field - Error Handling: Graceful fallback if encryption fails Security Considerations: - Message content only: Metadata remains unencrypted for search/indexing - Database admins: Can still see message metadata but not content - Key management: Critical - keys must be stored securely - Performance: Minimal overhead (~1-5ms per message) - Recovery: Lost keys mean lost message content [#967]
1 parent 933ad1b commit 60518e5

10 files changed

Lines changed: 1057 additions & 2 deletions

File tree

docker/tinode/config.template

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

keygen/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ A command-line utility to generate an API key for [Tinode server](../server/)
99
* `validate`: Key to validate: check previously issued key for validity.
1010
* `salt`: [HMAC](https://en.wikipedia.org/wiki/HMAC) salt, 32 random bytes base64 standard encoded; must be present for key validation; optional when generating the key: if missing, a cryptographically-strong salt will be automatically generated.
1111

12+
**Encryption Key Generation:**
13+
14+
* `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).
17+
1218

1319
## Usage
1420

@@ -30,6 +36,32 @@ API key v1 seq1 [ordinary]: AQAAAAABAACGOIyP2vh5avSff5oVvMpk
3036
HMAC salt: TC0Jzr8f28kAspXrb4UYccJUJ63b7CSA16n1qMxxGpw=
3137
```
3238

39+
**Generate Encryption Key:**
40+
41+
```sh
42+
# Generate 32-byte encryption key
43+
./keygen -encryption
44+
45+
# Generate custom size key
46+
./keygen -encryption -keysize 32
47+
48+
# Save key to file
49+
./keygen -encryption -output encryption.key
50+
```
51+
52+
Sample encryption key output:
53+
54+
```text
55+
Generated 32-byte encryption key:
56+
dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=
57+
58+
Add this to your tinode.conf:
59+
"encryption": {
60+
"enabled": true,
61+
"key": "dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q="
62+
}
63+
```
64+
3365
Copy `HMAC salt` to `api_key_salt` parameter in your server [config file](https://github.com/tinode/chat/blob/master/server/tinode.conf).
3466
Copy `API key` to the client applications:
3567

keygen/keygen.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,16 @@ func main() {
2626
apikey := flag.String("validate", "", "API key to validate")
2727
hmacSalt := flag.String("salt", "", "HMAC salt, 32 random bytes base64-encoded")
2828

29+
// Encryption key generation flags
30+
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)")
33+
2934
flag.Parse()
3035

31-
if *apikey != "" {
36+
if *encryptionKey {
37+
os.Exit(generateEncryptionKey(*keySize, *outputFile))
38+
} else if *apikey != "" {
3239
if *hmacSalt == "" {
3340
log.Println("Error: must provide HMAC salt for key validation")
3441
os.Exit(1)
@@ -172,3 +179,35 @@ func validate(apikey string, hmacSaltB64 string) int {
172179

173180
return 0
174181
}
182+
183+
// generateEncryptionKey generates a random encryption key of specified size
184+
func generateEncryptionKey(keySize int, outputFile string) int {
185+
// Generate random key
186+
key := make([]byte, keySize)
187+
if _, err := rand.Read(key); err != nil {
188+
log.Println("Error: Failed to generate random key", err)
189+
return 1
190+
}
191+
192+
// Encode to base64
193+
encodedKey := base64.StdEncoding.EncodeToString(key)
194+
195+
// 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,
208+
"key": "%s"
209+
}`, encodedKey)
210+
}
211+
212+
return 0
213+
}

server/main.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ 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
217221
}
218222

219223
// Credential validator config.
@@ -349,6 +353,10 @@ func main() {
349353
"Override the URL path where the server's internal status is displayed. Use '-' to disable.")
350354
pprofFile := flag.String("pprof", "", "File name to save profiling info to. Disabled if not set.")
351355
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)")
359+
352360
flag.Parse()
353361

354362
logs.Init(os.Stderr, *logFlags)
@@ -391,6 +399,10 @@ func main() {
391399
config.Listen = *listenOn
392400
}
393401

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

@@ -436,6 +448,11 @@ func main() {
436448
logs.Info.Printf("Profiling info saved to '%s.(cpu|mem)'", *pprofFile)
437449
}
438450

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

server/store/ENCRYPTION.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# Message Encryption
2+
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.
4+
5+
## Overview
6+
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.
8+
9+
## Configuration
10+
11+
### Configuration File
12+
13+
Add encryption settings to your `tinode.conf` file:
14+
15+
```json
16+
{
17+
"store_config": {
18+
"encryption": {
19+
"enabled": true,
20+
"key": "base64-encoded-32-byte-key-here"
21+
}
22+
}
23+
}
24+
```
25+
26+
### Command Line Flags
27+
28+
You can also enable encryption via command line flags:
29+
30+
```bash
31+
./tinode-server --encryption_enabled --encryption_key "base64-encoded-32-byte-key-here"
32+
```
33+
34+
## Key Management
35+
36+
### Generating an Encryption Key
37+
38+
Generate a 32-byte (256-bit) random key using the built-in keygen tool:
39+
40+
```bash
41+
# Generate 32-byte encryption key
42+
cd keygen
43+
./keygen -encryption
44+
45+
# Generate custom size key
46+
./keygen -encryption -keysize 32
47+
48+
# Save key to file
49+
./keygen -encryption -output encryption.key
50+
```
51+
52+
Alternatively, you can use OpenSSL:
53+
54+
```bash
55+
# Generate 32 random bytes and encode in base64
56+
openssl rand -base64 32
57+
```
58+
59+
### Key Storage
60+
61+
Store your encryption key securely:
62+
- Never commit encryption keys to version control
63+
- Use environment variables or secure key management systems
64+
- Consider using hardware security modules (HSMs) for production environments
65+
66+
## Migration
67+
68+
### From Unencrypted to Encrypted
69+
70+
To encrypt existing unencrypted messages, use the migration tool:
71+
72+
```bash
73+
# First, do a dry run to see what would be encrypted
74+
go run server/tools/encrypt_messages.go \
75+
--config tinode.conf \
76+
--key_string "your-base64-encoded-key" \
77+
--topic "your-topic-name" \
78+
--dry_run
79+
80+
# Then run the actual encryption
81+
go run server/tools/encrypt_messages.go \
82+
--config tinode.conf \
83+
--key_string "your-base64-encoded-key" \
84+
--topic "your-topic-name"
85+
```
86+
87+
### From Encrypted to Unencrypted
88+
89+
To decrypt encrypted messages (use with caution):
90+
91+
```bash
92+
go run server/tools/encrypt_messages.go \
93+
--config tinode.conf \
94+
--key_string "your-base64-encoded-key" \
95+
--topic "your-topic-name" \
96+
--reverse
97+
```
98+
99+
## Security Considerations
100+
101+
### What is Encrypted
102+
103+
- **Message content**: The actual text/content of messages
104+
- **Metadata**: Message headers, timestamps, sender info, etc. remain unencrypted
105+
106+
### What is NOT Encrypted
107+
108+
- Message metadata (sender, timestamp, sequence ID, etc.)
109+
- Topic information
110+
- User information
111+
- File attachments (planned for future versions)
112+
113+
### Limitations
114+
115+
- Database administrators can still see message metadata
116+
- The encryption key must be stored securely
117+
- If the key is lost, encrypted messages cannot be recovered
118+
- Encryption adds computational overhead
119+
120+
## Implementation Details
121+
122+
### Encryption Algorithm
123+
124+
- **Cipher**: AES-256
125+
- **Mode**: GCM (Galois/Counter Mode)
126+
- **Key size**: 256 bits (32 bytes)
127+
- **Nonce**: Random 12-byte nonce for each message
128+
129+
### Storage Format
130+
131+
Encrypted content is stored as a JSON object:
132+
133+
```json
134+
{
135+
"data": "base64-encoded-encrypted-data",
136+
"nonce": "base64-encoded-nonce",
137+
"encrypted": true
138+
}
139+
```
140+
141+
### Performance Impact
142+
143+
- **Encryption**: ~1-5ms per message (depending on content size)
144+
- **Decryption**: ~1-5ms per message (depending on content size)
145+
- **Storage overhead**: ~33% increase in content field size
146+
147+
## Troubleshooting
148+
149+
### Common Issues
150+
151+
1. **"encryption key is required when encryption is enabled"**
152+
- Ensure you've provided a valid base64-encoded 32-byte key
153+
154+
2. **"failed to decode base64 encryption key"**
155+
- Verify your key is properly base64-encoded
156+
- Ensure the key is exactly 32 bytes when decoded
157+
158+
3. **"failed to create AES cipher"**
159+
- This usually indicates a system-level issue with crypto libraries
160+
161+
### Logs
162+
163+
Encryption-related errors and warnings are logged with the prefix:
164+
- `topic[topic-name]: failed to encrypt message content (seq: X) - err: ...`
165+
- `topic[topic-name]: failed to decrypt message content (seq: X) - err: ...`
166+
167+
## Future Enhancements
168+
169+
- File attachment encryption
170+
- Key rotation support
171+
- Hardware security module (HSM) integration
172+
- Per-topic encryption settings
173+
- End-to-end encryption support
174+
175+
## API Changes
176+
177+
No changes to the client API are required. Messages are automatically encrypted/decrypted transparently.
178+
179+
## Testing
180+
181+
To test encryption functionality:
182+
183+
1. Start the server with encryption enabled
184+
2. Send messages through the normal API
185+
3. Verify messages are encrypted in the database
186+
4. Verify messages are decrypted when retrieved
187+
5. Check that encryption/decryption errors are properly logged

0 commit comments

Comments
 (0)