-
Notifications
You must be signed in to change notification settings - Fork 2k
#967: feature/encrypt message at rest #971
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: devel
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,11 @@ A command-line utility to generate an API key for [Tinode server](../server/) | |
* `validate`: Key to validate: check previously issued key for validity. | ||
* `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. | ||
|
||
**Encryption Key Generation:** | ||
|
||
* `encryption`: Generate encryption key instead of API key. | ||
* `keysize`: Encryption key size in bytes. Must be 16 (AES-128), 24 (AES-192), or 32 (AES-256). Default: 32. | ||
|
||
|
||
## Usage | ||
|
||
|
@@ -30,6 +35,34 @@ API key v1 seq1 [ordinary]: AQAAAAABAACGOIyP2vh5avSff5oVvMpk | |
HMAC salt: TC0Jzr8f28kAspXrb4UYccJUJ63b7CSA16n1qMxxGpw= | ||
``` | ||
|
||
**Generate Encryption Key:** | ||
|
||
```sh | ||
# Generate 32-byte encryption key (AES-256) | ||
./keygen -encryption | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please rename for consistency to be less generic. |
||
|
||
# Generate 16-byte encryption key (AES-128) | ||
./keygen -encryption -keysize 16 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please rename |
||
|
||
# Generate 24-byte encryption key (AES-192) | ||
./keygen -encryption -keysize 24 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example is probably unnecessary. Any reasonable person can guess that specifying |
||
|
||
# Save key to file using shell redirection | ||
./keygen -encryption > encryption.key | ||
``` | ||
|
||
Sample encryption key output: | ||
|
||
```text | ||
Generated 32-byte encryption key: | ||
dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q= | ||
|
||
Add this to your tinode.conf: | ||
"encrypt_at_rest": { | ||
"key": "dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=" | ||
} | ||
``` | ||
|
||
Copy `HMAC salt` to `api_key_salt` parameter in your server [config file](https://github.com/tinode/chat/blob/master/server/tinode.conf). | ||
Copy `API key` to the client applications: | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,9 +26,15 @@ func main() { | |
apikey := flag.String("validate", "", "API key to validate") | ||
hmacSalt := flag.String("salt", "", "HMAC salt, 32 random bytes base64-encoded") | ||
|
||
// Encryption key generation flags | ||
encryptionKey := flag.Bool("encryption", false, "Generate encryption key instead of API key") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please rename |
||
keySize := flag.Int("keysize", 32, "Encryption key size in bytes (16, 24, or 32 for AES-128/192/256)") | ||
|
||
flag.Parse() | ||
|
||
if *apikey != "" { | ||
if *encryptionKey { | ||
os.Exit(generateEncryptionKey(*keySize)) | ||
} else if *apikey != "" { | ||
if *hmacSalt == "" { | ||
log.Println("Error: must provide HMAC salt for key validation") | ||
os.Exit(1) | ||
|
@@ -172,3 +178,32 @@ func validate(apikey string, hmacSaltB64 string) int { | |
|
||
return 0 | ||
} | ||
|
||
// generateEncryptionKey generates a random encryption key of specified size | ||
func generateEncryptionKey(keySize int) int { | ||
// Validate key size - AES supports 16, 24, or 32 bytes | ||
if keySize != 16 && keySize != 24 && keySize != 32 { | ||
log.Printf("Error: Invalid key size %d. Must be 16 (AES-128), 24 (AES-192), or 32 (AES-256) bytes", keySize) | ||
return 1 | ||
} | ||
|
||
// Generate random key | ||
or-else marked this conversation as resolved.
Show resolved
Hide resolved
|
||
key := make([]byte, keySize) | ||
if _, err := rand.Read(key); err != nil { | ||
log.Println("Error: Failed to generate random key", err) | ||
return 1 | ||
} | ||
|
||
// Encode to base64 | ||
encodedKey := base64.StdEncoding.EncodeToString(key) | ||
|
||
// Output | ||
fmt.Printf("Generated %d-byte encryption key:\n", keySize) | ||
fmt.Printf("%s\n", encodedKey) | ||
fmt.Printf("\nAdd this to your tinode.conf:\n") | ||
fmt.Printf(`"encrypt_at_rest": { | ||
"key": "%s" | ||
}`, encodedKey) | ||
|
||
return 0 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
# Message Encryption at Rest | ||
|
||
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. | ||
|
||
## Overview | ||
|
||
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. | ||
|
||
**Supported AES key sizes:** | ||
|
||
- AES-128: 16 bytes (128 bits) | ||
- AES-192: 24 bytes (192 bits) | ||
- AES-256: 32 bytes (256 bits) | ||
|
||
## Configuration | ||
|
||
### Configuration File | ||
|
||
Add encryption settings to your `tinode.conf` file: | ||
|
||
```json | ||
{ | ||
"store_config": { | ||
"encrypt_at_rest": { | ||
"key": "base64-encoded-key-here" | ||
} | ||
} | ||
} | ||
``` | ||
|
||
**Note:** If no key is provided or the key is empty, encryption is disabled. | ||
|
||
### Command Line Flags | ||
|
||
You can also enable encryption via command line flags (overrides config file): | ||
|
||
```bash | ||
./tinode-server --message_encrypt_at_rest_key "base64-encoded-key-here" | ||
``` | ||
|
||
## Key Management | ||
|
||
### Generating an Encryption Key | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe just link to this section from |
||
Generate a random key using the built-in keygen tool: | ||
|
||
```bash | ||
# Generate 32-byte encryption key (AES-256) | ||
cd keygen | ||
./keygen -encryption | ||
|
||
# Generate 16-byte encryption key (AES-128) | ||
./keygen -encryption -keysize 16 | ||
|
||
# Generate 24-byte encryption key (AES-192) | ||
./keygen -encryption -keysize 24 | ||
|
||
# Save key to file using shell redirection | ||
./keygen -encryption > encryption.key | ||
``` | ||
|
||
The keygen tool validates that the key size is exactly 16, 24, or 32 bytes. | ||
|
||
Alternatively, you can use OpenSSL: | ||
|
||
```bash | ||
# Generate 16 random bytes and encode in base64 (AES-128) | ||
openssl rand -base64 16 | ||
|
||
# Generate 24 random bytes and encode in base64 (AES-192) | ||
openssl rand -base64 24 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need for 3 examples. One example is perfectly adequate. |
||
|
||
# Generate 32 random bytes and encode in base64 (AES-256) | ||
openssl rand -base64 32 | ||
``` | ||
|
||
### Key Storage | ||
|
||
Store your encryption key securely: | ||
- Never commit encryption keys to version control | ||
- Use environment variables or secure key management systems | ||
- Consider using hardware security modules (HSMs) for production environments | ||
|
||
## Migration | ||
|
||
### From Unencrypted to Encrypted | ||
|
||
To encrypt existing unencrypted messages, use the migration tool: | ||
|
||
```bash | ||
# First, do a dry run to see what would be encrypted | ||
go run server/tools/encrypt_messages.go \ | ||
--config tinode.conf \ | ||
--key_string "your-base64-encoded-key" \ | ||
--topic "your-topic-name" \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Think how normal admin would do it. Wony would anyone want to do it for a single topic? I can't imagine much use in that. I can see how someone would want to encryp all the messages. What you are doing here is an artefact of the store API. It does not provide a method to get all messages for all topics. Just add a method specifically for this. |
||
--dry_run | ||
|
||
# Then run the actual encryption | ||
go run server/tools/encrypt_messages.go \ | ||
--config tinode.conf \ | ||
--key_string "your-base64-encoded-key" \ | ||
--topic "your-topic-name" | ||
``` | ||
|
||
**Note:** The migration tool now uses the proper store interface and handles all supported AES key sizes. | ||
|
||
### From Encrypted to Unencrypted | ||
|
||
To decrypt encrypted messages (use with caution): | ||
|
||
```bash | ||
go run server/tools/encrypt_messages.go \ | ||
--config tinode.conf \ | ||
--key_string "your-base64-encoded-key" \ | ||
--topic "your-topic-name" \ | ||
--reverse | ||
``` | ||
|
||
## Security Considerations | ||
|
||
### What is Encrypted | ||
|
||
- **Message content**: The actual text/content of messages | ||
- **Metadata**: Message headers, timestamps, sender info, etc. remain unencrypted | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confusing: the title is Whats encrypted, this line is what's not encrypted. |
||
|
||
### What is NOT Encrypted | ||
|
||
- Message metadata (sender, timestamp, sequence ID, etc.) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is duplicate. You have the same statement above. |
||
- Topic information | ||
- User information | ||
- File attachments (planned for future versions) | ||
|
||
### Limitations | ||
|
||
- Database administrators can still see message metadata | ||
- The encryption key must be stored securely | ||
- If the key is lost, encrypted messages cannot be recovered | ||
- Encryption adds computational overhead | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be a good idea to add a reference to the |
||
|
||
## Implementation Details | ||
|
||
### Encryption Algorithm | ||
|
||
- **Cipher**: AES (Advanced Encryption Standard) | ||
- **Mode**: GCM (Galois/Counter Mode) | ||
- **Key sizes**: 128, 192, or 256 bits (16, 24, or 32 bytes) | ||
- **Nonce**: Random 12-byte nonce for each message | ||
|
||
### Storage Format | ||
|
||
Encrypted content is stored as a JSON object with automatic base64 encoding: | ||
|
||
```json | ||
{ | ||
"data": "base64-encoded-encrypted-data", | ||
"nonce": "base64-encoded-nonce", | ||
"encrypted": true | ||
} | ||
``` | ||
|
||
The `data` and `nonce` fields are automatically base64 encoded/decoded during JSON marshaling/unmarshaling. | ||
|
||
### Performance Impact | ||
|
||
- **Encryption**: ~1-5ms per message (depending on content size) | ||
- **Decryption**: ~1-5ms per message (depending on content size) | ||
- **Storage overhead**: ~33% increase in content field size | ||
|
||
## Troubleshooting | ||
|
||
### Common Issues | ||
|
||
1. **"encryption key must be 16, 24, or 32 bytes"** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's an issue. |
||
- Ensure your key is exactly 16, 24, or 32 bytes when decoded from base64 | ||
- Use the keygen tool to generate valid keys | ||
|
||
2. **"failed to decode base64 encryption key"** | ||
- Verify your key is properly base64-encoded | ||
- Ensure there are no extra spaces or newlines in the key | ||
|
||
3. **"failed to create AES cipher"** | ||
- This usually indicates a system-level issue with crypto libraries | ||
|
||
### Logs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's basically not useful in this form. Essentially saying "there could be errors". OK. What do users do with it? |
||
|
||
Encryption-related errors and warnings are logged with the prefix: | ||
- `topic[topic-name]: failed to encrypt message content (seq: X) - err: ...` | ||
- `topic[topic-name]: failed to decrypt message content (seq: X) - err: ...` | ||
|
||
## Future Enhancements | ||
|
||
- File attachment encryption | ||
- Key rotation support | ||
- Hardware security module (HSM) integration | ||
- Per-topic encryption settings | ||
- End-to-end encryption support | ||
|
||
## Migration from Previous Versions | ||
|
||
**New configuration:** | ||
|
||
```json | ||
{ | ||
"store_config": { | ||
"encrypt_at_rest": { | ||
"key": "base64-encoded-key" | ||
} | ||
} | ||
} | ||
``` | ||
|
||
**Key changes:** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove this section. There is no value in it because the previous version has never been released. |
||
- Support for multiple AES key sizes (16, 24, 32 bytes) instead of just 32 bytes | ||
- Command line flag renamed from `--encryption_key` to `--message_encrypt_at_rest_key` | ||
|
||
### Migration Steps | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would say the first step should be backing up the database. |
||
1. **Update your configuration file** to use the new field names | ||
2. **Test with a dry run** using the migration tool | ||
3. **Restart the server** with the new configuration | ||
4. **Verify encryption is working** by checking logs and database content | ||
|
||
## API Changes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don;t think this has any value to end-users. Please remove. |
||
|
||
No changes to the client API are required. Messages are automatically encrypted/decrypted transparently. | ||
|
||
## Testing | ||
|
||
To test encryption functionality: | ||
|
||
1. Start the server with encryption enabled | ||
2. Send messages through the normal API | ||
3. Verify messages are encrypted in the database | ||
4. Verify messages are decrypted when retrieved | ||
5. Check that encryption/decryption errors are properly logged |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likewise, 'encryption' is too generic. I would use something like above:
encypt_at_rest
orstore_encrypt
or some such.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any change