Skip to content

Commit 2a87eff

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 933ad1b commit 2a87eff

File tree

9 files changed

+1056
-4
lines changed

9 files changed

+1056
-4
lines changed

keygen/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ 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+
**Message Encryption at Rest Key Generation:**
13+
14+
* `encrypt_at_rest`: Generate encryption key for message encryption at rest instead of API key.
15+
* `keysize`: Encryption key size in bytes. Must be 16 (AES-128), 24 (AES-192), or 32 (AES-256). Default: 32.
16+
1217

1318
## Usage
1419

@@ -30,6 +35,34 @@ API key v1 seq1 [ordinary]: AQAAAAABAACGOIyP2vh5avSff5oVvMpk
3035
HMAC salt: TC0Jzr8f28kAspXrb4UYccJUJ63b7CSA16n1qMxxGpw=
3136
```
3237

38+
**Generate Message Encryption at Rest Key:**
39+
40+
```sh
41+
# Generate 32-byte key (AES-256)
42+
./keygen -encrypt_at_rest
43+
44+
# Generate 16-byte key (AES-128)
45+
./keygen -encrypt_at_rest -keysize 16
46+
47+
# Generate 24-byte key (AES-192)
48+
./keygen -encrypt_at_rest -keysize 24
49+
50+
# Save key to file using shell redirection
51+
./keygen -encrypt_at_rest > encrypt_at_rest.key
52+
```
53+
54+
Sample output:
55+
56+
```text
57+
Generated 32-byte encryption key:
58+
dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=
59+
60+
Add this to your tinode.conf:
61+
"encrypt_at_rest": {
62+
"key": "dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q="
63+
}
64+
```
65+
3366
Copy `HMAC salt` to `api_key_salt` parameter in your server [config file](https://github.com/tinode/chat/blob/master/server/tinode.conf).
3467
Copy `API key` to the client applications:
3568

keygen/keygen.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@ 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+
// Message encryption at rest key generation flags
30+
encryptAtRest := flag.Bool("encrypt_at_rest", false, "Generate encryption key for message encryption at rest")
31+
keySize := flag.Int("keysize", 32, "Encryption key size in bytes (16, 24, or 32 for AES-128/192/256)")
32+
2933
flag.Parse()
3034

31-
if *apikey != "" {
35+
if *encryptAtRest {
36+
os.Exit(generateEncryptAtRestKey(*keySize))
37+
} else if *apikey != "" {
3238
if *hmacSalt == "" {
3339
log.Println("Error: must provide HMAC salt for key validation")
3440
os.Exit(1)
@@ -172,3 +178,33 @@ func validate(apikey string, hmacSaltB64 string) int {
172178

173179
return 0
174180
}
181+
182+
// generateEncryptAtRestKey generates a random encryption key for message encryption at rest
183+
func generateEncryptAtRestKey(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+
190+
// Generate random key
191+
key := make([]byte, keySize)
192+
if _, err := rand.Read(key); err != nil {
193+
log.Println("Error: Failed to generate random key", err)
194+
return 1
195+
}
196+
197+
// Encode to base64
198+
encodedKey := base64.StdEncoding.EncodeToString(key)
199+
200+
// Output
201+
fmt.Printf("Generated %d-byte encryption at rest key:\n", keySize)
202+
fmt.Printf("%s\n", encodedKey)
203+
fmt.Printf("\nAdd this to your tinode.conf store_config:\n")
204+
fmt.Printf(`"encrypt_at_rest": {
205+
"key": "%s"
206+
}
207+
`, encodedKey)
208+
209+
return 0
210+
}

server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ func main() {
349349
"Override the URL path where the server's internal status is displayed. Use '-' to disable.")
350350
pprofFile := flag.String("pprof", "", "File name to save profiling info to. Disabled if not set.")
351351
pprofUrl := flag.String("pprof_url", "", "Debugging only! URL path for exposing profiling info. Disabled if not set.")
352+
352353
flag.Parse()
353354

354355
logs.Init(os.Stderr, *logFlags)

server/store/ENCRYPTION.md

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

0 commit comments

Comments
 (0)