Skip to content

Conversation

ercsnmrs
Copy link
Contributor

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]

@ercsnmrs
Copy link
Contributor Author

I will post the test results + load test.

@or-else
Copy link
Contributor

or-else commented Aug 31, 2025

Do you want me to review this now or later, after the tests?

@ercsnmrs
Copy link
Contributor Author

@or-else please do after my testing.

@ercsnmrs
Copy link
Contributor Author

@or-else thinking about it, I would appreciate early feedbacks, we can discuss more technical details afterwards.


**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


// Encrypt message content if encryption is enabled
if encryptionService != nil && encryptionService.IsEnabled() {
encryptedContent, err := encryptionService.EncryptContent(msg.Content)
Copy link
Contributor

Choose a reason for hiding this comment

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

if encryptedContent, err := encryptionService.EncryptContent(msg.Content); err != nil { ...

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.

No, not really.

return adp.MessageGetAll(topic, forUser, opt)
messages, err := adp.MessageGetAll(topic, forUser, opt)
if err != nil {
return nil, err
Copy link
Contributor

Choose a reason for hiding this comment

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

Return messages, err rather than nil, err. No need to change the logic here.

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 the change.

}

// Get messages for the specified topic
messages, err := store.Store.GetAdapter().MessageGetAll(*topicName, types.ZeroUid, nil)
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 not going to work. You need a new method in the adapter interface for this.

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.

Please do not say done. This is still completely not going to work. If you need help, please ask.

mielen636-ui
mielen636-ui previously approved these changes Sep 5, 2025
@or-else or-else changed the base branch from master to devel September 5, 2025 10:35
@or-else or-else dismissed mielen636-ui’s stale review September 5, 2025 10:35

Please don't do this.

@or-else
Copy link
Contributor

or-else commented Sep 15, 2025

Any update on this?

@or-else
Copy link
Contributor

or-else commented Sep 19, 2025

Closing as abandoned

@or-else or-else closed this Sep 19, 2025
@ercsnmrs
Copy link
Contributor Author

@or-else let me open another mr on this one, been a though week. thanks for your input.

@or-else or-else reopened this Sep 19, 2025
@or-else
Copy link
Contributor

or-else commented Sep 19, 2025

If you want to continue, please use this PR. I reopened it. Thanks.

@ercsnmrs
Copy link
Contributor Author

Appreciate it thanks!

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

[tinode#967]
@ercsnmrs ercsnmrs force-pushed the feature/encrypt-message-at-rest branch 2 times, most recently from 5d3ce78 to 157476a Compare September 20, 2025 05:01
@or-else
Copy link
Contributor

or-else commented Sep 20, 2025

Could you please respond to each of my comments with done (if resolved) or not done and an explanation why you think it's not a good idea. This way I would know what to look for in the next code review. Thanks!

@ercsnmrs
Copy link
Contributor Author

@or-else sure, currently testing the changes. thanks!

… 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

[tinode#967]
@ercsnmrs ercsnmrs force-pushed the feature/encrypt-message-at-rest branch from 157476a to 4ae0494 Compare September 20, 2025 07:25
@ercsnmrs
Copy link
Contributor Author

Testing Results:

Steps:

  1. Generated and added the encrypted_at_rest config on tinode.conf file
Screenshot 2025-09-20 at 15 30 48
  1. Send Message 1 - Testing User
Screenshot 2025-09-20 at 15 19 11 Screenshot 2025-09-20 at 15 19 11

Result: Encrypted = true

  1. Send Message 2 - second encrypted message
Screenshot 2025-09-20 at 15 20 06

Result: Encrypted = true

  1. Revert Config from Step 1
Screenshot 2025-09-20 at 15 23 44 Screenshot 2025-09-20 at 15 23 27

Result: Encrypted = false

Some observation:

  • I need to refresh the webapp to display the chat
  • Needs thorough testing

CC: @or-else

@ercsnmrs ercsnmrs marked this pull request as ready for review September 20, 2025 07:46

**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.

I don't see any change


```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.

./keygen -encryption

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

./keygen -encryption -keysize 16

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

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.

msg.SetUid(Store.GetUid())

// Encrypt message content if encryption is enabled
if messageEncryptionService != nil && messageEncryptionService.IsEnabled() {
Copy link
Contributor

Choose a reason for hiding this comment

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

The messageEncryptionService != nil is redundant. You already check for it in messageEncryptionService.IsEnabled(). This is Go, the nil pointer is not going to be dereferenced for calling IsEnabled().

return adp.MessageGetAll(topic, forUser, opt)
messages, err := adp.MessageGetAll(topic, forUser, opt)
if err != nil {
return nil, err
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 the change.

}

// Decrypt message content if encryption is enabled
if messageEncryptionService != nil && messageEncryptionService.IsEnabled() {
Copy link
Contributor

Choose a reason for hiding this comment

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

The messageEncryptionService != nil is redundant.

@@ -0,0 +1,131 @@
# Encryption Tools
Copy link
Contributor

Choose a reason for hiding this comment

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

Please reduce duplication. Copy-pasting saves time short-term but creates problems in the long run. It requires multiple documents to be updated for every change.

}

// Get messages for the specified topic using the store interface
messages, err := store.Messages.GetAll(*topicName, types.ZeroUid, nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

As I said before, this is totally not going to work. Messages.GetAll is not suitable for the task.
For one, Messages.GetAll will return just 1024 messages from the topic.

This part of code in present form is not useful at all. If you don't know how to fix it, please talk to me or just remove it. This is just going to confuse users and won't do anything useful.

Copy link
Contributor

Choose a reason for hiding this comment

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

You need to create a new DB method to paginate over all messages in the DB. Get a page (10K messages?), encrypt/decrypt them, write back, then do the next page and so on.

keyFile = flag.String("key", "", "Path to file containing base64-encoded 32-byte encryption key")
key = flag.String("key_string", "", "Base64-encoded 32-byte encryption key as string")
dryRun = flag.Bool("dry_run", false, "Show what would be done without making changes")
reverse = flag.Bool("reverse", false, "Decrypt encrypted messages (use with caution)")
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not call this flag decrypt?

key = flag.String("key_string", "", "Base64-encoded 32-byte encryption key as string")
dryRun = flag.Bool("dry_run", false, "Show what would be done without making changes")
reverse = flag.Bool("reverse", false, "Decrypt encrypted messages (use with caution)")
topicName = flag.String("topic", "", "Topic name to process (leave empty to process all accessible topics)")
Copy link
Contributor

@or-else or-else Sep 20, 2025

Choose a reason for hiding this comment

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

Doing it topic-by-topic is not useful.

}

// Get encryption key
var encryptionKey []byte
Copy link
Contributor

Choose a reason for hiding this comment

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

As I said before, the key from the config file will override the command line key. That should be exacly inverse: the command line should override the configFile.


if !*dryRun {
// Save the modified message using the store interface
err, _ := store.Messages.Save(&msg, nil, false)
Copy link
Contributor

Choose a reason for hiding this comment

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

This will insert a new message into the DB.

@or-else
Copy link
Contributor

or-else commented Sep 20, 2025

Please move server/tools/encrypt_messages.go to another PR. It will require a lot of work. It can be done later.

@ercsnmrs ercsnmrs marked this pull request as draft September 21, 2025 03:57
@ercsnmrs
Copy link
Contributor Author

@or-else i will continue on this, will push a change tomorrow oct 10. please keep it open. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants