Encryption
Wirechat can encrypt message bodies at rest so plaintext messages are not stored directly in your database or database backups.
This is database encryption, not end-to-end encryption. Your Laravel application can still decrypt messages when it needs to render the chat UI, send notifications, moderate content, run AI workflows, or build future workflow features.
Enable Encryption
Publish the Wirechat config if you have not already:
php artisan vendor:publish --tag=wirechat-config
Then enable message encryption:
'encryption' => [
'enabled' => env('WIRECHAT_ENCRYPTION_ENABLED', false),
'compression' => [
'enabled' => true,
'min_bytes' => 512,
'level' => 6,
'require_smaller_output' => true,
],
],
In production, set:
WIRECHAT_ENCRYPTION_ENABLED=true
PHP Extensions
Wirechat encryption uses Laravel's encryption service, which is backed by PHP's OpenSSL extension.
Laravel already requires ext-openssl, so most Laravel applications have this installed before Wirechat is added. If your server is missing OpenSSL, enable or install the PHP OpenSSL extension before enabling encryption.
Wirechat compression uses gzip through PHP's zlib extension.
The zlib extension provides gzencode() and gzdecode(). It is broadly available on normal PHP installs, but it is still an extension your server must have if you want compression.
If zlib is not available, Wirechat skips compression for new encrypted messages and stores them as encrypted-only payloads. If an existing message was already stored with compression = gzip, zlib is required to read it. Without zlib, Wirechat throws a controlled decrypt exception instead of causing a PHP fatal error.
Using The Service
Most applications do not need to call the encryption service directly. Wirechat encrypts and decrypts message bodies through the Message model automatically.
If your application needs to encrypt Wirechat-related strings for internal workflows, use the facade:
use Wirechat\Wirechat\Facades\WirechatEncryption;
-
Encrypt a simple string
encryptString()returns the original value when encryption is disabled. When encryption is enabled, it returns a prefixed encrypted value.
$encrypted = WirechatEncryption::encryptString('Internal note');
-
Decrypt a simple string
decryptString()returns plaintext values unchanged. If the value starts withwcenc:v1:, Wirechat decrypts it even when encryption is disabled in config.
$plaintext = WirechatEncryption::decryptString($encrypted);
-
Store a message-like encrypted value
Use
encryptStringForStorage()when the encrypted value may need Wirechat metadata, such as compression mode. This is the same style Wirechat uses for message bodies.
$stored = WirechatEncryption::encryptStringForStorage(
'Long internal message body...',
['source' => 'workflow']
);
$body = $stored['body'];
$meta = $stored['meta'];
The returned values can be stored in your own columns if you follow the same shape:
[
'body' => 'wcenc:v1:...',
'meta' => [
'source' => 'workflow',
'encryption' => [
'v' => 1,
'compression' => 'none',
],
],
]
-
Decrypt a message-like stored value
Pass the stored body and meta back together so Wirechat knows whether it must decompress after decrypting.
$plaintext = WirechatEncryption::decryptStringFromStorage($body, $meta);
For custom storage, prefer the storage methods when you want Wirechat's compression behavior. Use the simple string methods only when you do not need compression metadata.
Configuration Reference
The encryption config is intentionally small:
-
Enabled
Controls whether new message bodies are encrypted before they are stored. Set
WIRECHAT_ENCRYPTION_ENABLED=trueto enable encryption. -
Compression enabled
Controls whether Wirechat may compress large message bodies before encryption.
-
Compression minimum bytes
The minimum plaintext body size, in bytes, before Wirechat considers compression. The default is
512, so short chat messages are encrypted without compression. -
Compression level
The gzip compression level from
0to9. Higher values can compress more but use more CPU. The default is6. -
Require smaller output
When
true, Wirechat only stores the compressed version if it is smaller than the original plaintext body.
Wirechat follows Laravel's encryption key behavior. On Laravel versions that support graceful encryption key rotation, you may rotate APP_KEY as long as previous keys are configured with APP_PREVIOUS_KEYS. New message bodies will be encrypted with the current key, while Laravel can still try previous keys when decrypting older payloads.
Deep Dive
Wirechat does not implement custom cryptography. It uses Laravel's encryption service:
Crypt::encryptString($value);
Crypt::decryptString($payload);
Wirechat intentionally uses Crypt::encryptString() instead of Laravel's encrypt() helper. Crypt::encryptString() avoids PHP serialization and is cleaner for message-body strings.
Laravel Crypt uses PHP OpenSSL under the hood.
That means the actual encryption work is handled by the OpenSSL extension that Laravel already depends on. Wirechat does not ship a separate encryption library and does not implement its own cipher logic.
Laravel's encrypter is backed by your application APP_KEY and config('app.cipher'). In a normal Laravel application, the cipher is usually AES-256-CBC. Laravel also supports other configured ciphers, such as AES CBC and GCM variants, depending on your app config and Laravel version.
Wirechat does not choose the low-level cipher itself. The wcenc:v1: marker means "Wirechat encrypted body format version 1", and version 1 uses Laravel Crypt. Laravel decides the actual OpenSSL cipher from the application encryption config.
For CBC ciphers, Laravel's encrypted payload includes an initialization vector and a message authentication code, so encrypted values are protected against tampering. For AEAD ciphers such as GCM, Laravel uses the authentication tag provided by OpenSSL.
Save Flow
When a message is saved, Wirechat:
- Detects the message type from the plaintext body.
- Optionally compresses large message bodies.
- Encrypts the body with Laravel encryption.
- Stores the encrypted payload in the existing
bodycolumn. - Stores non-sensitive encryption metadata in the message
metacolumn.
Encrypted values look like this:
wcenc:v1:{Laravel encrypted payload}
The wcenc:v1: prefix is plain text. It is only a Wirechat marker that lets the model quickly detect encrypted rows. The encrypted payload after the prefix is produced by Laravel Crypt::encryptString().
The message meta column stores the instructions Wirechat needs to read the encrypted value:
[
'encryption' => [
'v' => 1,
'compression' => 'none',
],
]
-
Version
The Wirechat encrypted body format version. The current value is
1. Version 1 means LaravelCrypt::encryptString()andCrypt::decryptString()with the application's normal encryption key and cipher. -
Compression
The compression mode used before encryption. This is either
noneorgzip. Wirechat needs this value during reads so it knows whether to decompress after decryption. -
Encrypted payload
The sensitive value stored in
bodyafterwcenc:v1:. If compression was used, the payload contains encrypted compressed bytes. If compression was not used, it contains the encrypted plaintext message body.
The meta.encryption values are not secrets. They do not contain plaintext, keys, or ciphertext. They only describe how Wirechat should process the encrypted payload.
Read Flow
When a message is read through the Wirechat model, Wirechat detects the prefix, decrypts the value, decompresses it when needed, and returns the original plaintext body.
Existing plaintext messages remain readable. Encrypted messages also remain readable if encryption is later disabled, because Wirechat detects encrypted values by prefix.
The read flow is:
body column
-> detect wcenc:v1:
-> read meta.encryption.compression
-> decrypt the body payload with Laravel Crypt
-> if meta compression is gzip, decompress with gzdecode()
-> return plaintext body
Compression
Compression is a storage optimization only. It is not encryption and should not be treated as a security feature.
By default, Wirechat only considers compression for message bodies of at least 512 bytes and only keeps the compressed value when it is smaller than the original body.
Small chat messages are encrypted without compression.
Wirechat uses gzip compression through PHP zlib.
If your PHP runtime does not have ext-zlib, Wirechat automatically skips compression for new messages. To use compression, install or enable the zlib extension for the same PHP runtime that serves your Laravel application.
Wirechat compresses before encryption because encrypted output does not compress well:
plaintext -> optional gzip -> Laravel encryption -> body payload + message meta
The opposite order is not useful:
plaintext -> Laravel encryption -> gzip
Transport Security
Wirechat does not add a custom WebSocket encryption layer.
Use HTTPS and secure WebSockets:
https://
wss://
TLS protects messages in transit. Wirechat encryption protects stored message bodies at rest.
Caveats
Encrypted message bodies cannot be searched with database LIKE queries because the stored body value is no longer plaintext.
Wirechat does not currently provide message body search. If your application adds message search later, use a separate search or indexing strategy that is designed for encrypted storage.
Because this is not end-to-end encryption, anyone with application-level decryption access and the correct Laravel key can decrypt message bodies. This is intentional so Wirechat can continue supporting moderation, notifications, AI integrations, multi-device access, and future workflow systems.
If your Laravel version or application configuration does not support previous encryption keys, changing APP_KEY will prevent old encrypted message bodies from being decrypted.