Skip to content

Commit 1a92f1a

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 1a92f1a

9 files changed

Lines changed: 1083 additions & 4 deletions

File tree

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+
**Encryption Key Generation:**
13+
14+
* `encryption`: Generate encryption key 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 Encryption Key:**
39+
40+
```sh
41+
# Generate 32-byte encryption key (AES-256)
42+
./keygen -encryption
43+
44+
# Generate 16-byte encryption key (AES-128)
45+
./keygen -encryption -keysize 16
46+
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
52+
```
53+
54+
Sample encryption key 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: 36 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+
// 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 (16, 24, or 32 for AES-128/192/256)")
32+
2933
flag.Parse()
3034

31-
if *apikey != "" {
35+
if *encryptionKey {
36+
os.Exit(generateEncryptionKey(*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,32 @@ func validate(apikey string, hmacSaltB64 string) int {
172178

173179
return 0
174180
}
181+
182+
// generateEncryptionKey generates a random encryption key of specified size
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+
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 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": {
205+
"key": "%s"
206+
}`, encodedKey)
207+
208+
return 0
209+
}

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

0 commit comments

Comments
 (0)