Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions keygen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

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 or store_encrypt or some such.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

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

* `keysize`: Encryption key size in bytes. Must be 16 (AES-128), 24 (AES-192), or 32 (AES-256). Default: 32.


## Usage

Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename encryption for consistency to be less generic.


# Generate 24-byte encryption key (AES-192)
./keygen -encryption -keysize 24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is probably unnecessary. Any reasonable person can guess that specifying -keysize 24 would mean a 24-byte key.


# 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:

Expand Down
37 changes: 36 additions & 1 deletion keygen/keygen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename encryption for consistency to be less generic.

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)
Expand Down Expand Up @@ -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
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
}
1 change: 1 addition & 0 deletions server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ func main() {
"Override the URL path where the server's internal status is displayed. Use '-' to disable.")
pprofFile := flag.String("pprof", "", "File name to save profiling info to. Disabled if not set.")
pprofUrl := flag.String("pprof_url", "", "Debugging only! URL path for exposing profiling info. Disabled if not set.")

flag.Parse()

logs.Init(os.Stderr, *logFlags)
Expand Down
235 changes: 235 additions & 0 deletions server/store/ENCRYPTION.md
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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just link to this section from keygen/README.md instead of duplicating the text. With duplicate text any change has to be done twice. I would rather avoid it.

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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" \
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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.)
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a good idea to add a reference to the Performance Impact section below.


## 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"**
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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:**
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Loading