Encrypted alerts
With end-to-end encryption, the sender encrypts the alert’s title, body, and link before sending, and only devices that hold the channel key can read them. The Gregor server stores and delivers the ciphertext but never sees the content.
The server does not store, distribute, verify, or recover channel keys. You copy the key to each sender and device yourself, and a device without the key cannot read the alerts.
Turn it on
Section titled “Turn it on”- Open the channel’s actions on the Channels page and choose Channel settings.
- Under End-to-end encryption, turn on Require encrypted messages and save. From now on the channel rejects plaintext alerts with
encryption_required. - Reopen the channel settings and choose Manage channel key, then Generate key.
- Choose Copy key and give the key to your senders and to the other devices that should read the alerts. On each of those devices, open Manage channel key and choose Import key.
A channel that does not require encryption accepts both plaintext and encrypted alerts.
The exported key looks like this:
ck1:<channel_id>:<key_version>:<key><key> is 32 random bytes in unpadded base64url. Treat the whole string as a secret.
Rotate the key
Section titled “Rotate the key”Rotate key creates a new key with the next version number. Devices keep the older versions, so alerts encrypted earlier stay readable. Copy the new key to senders and devices again; Gregor does not do it for you.
Who receives encrypted alerts
Section titled “Who receives encrypted alerts”The channel settings control delivery and display of encrypted alerts:
- Encrypted delivery: All recipients, Selected recipients only, or Exclude selected recipients. This controls who is sent encrypted alerts. The server cannot know who actually holds the key.
- Without a local key: Show placeholder shows that an encrypted alert arrived but cannot be read; Hide from normal views hides it.
Heartbeats are not available on channels that require encryption, because the server writes heartbeat alerts and cannot encrypt them.
Send an encrypted alert
Section titled “Send an encrypted alert”The command-line tool does all of this for you: set CHANNEL_KEY and send as usual. To write your own sender, follow the steps below.
Encrypt a JSON object with the alert content using XChaCha20-Poly1305:
{ "title": "Backup failed", "body": "nas: exit code 2", "url": "https://nas.example.com" }The associated data (AAD) is this JSON object, encoded as UTF-8. Its values must equal the request’s outer fields, use empty strings for fields you do not send, and devices reject alerts whose AAD does not match:
{ "schema": "alert-inbox.message-aad.v1", "channel_id": "<channel_id>", "key_version": 1, "event_type": "backup.failed", "event_subject": "host:nas", "thread_key": ""}Send the result in place of alert, with binary values in unpadded base64url. ciphertext is the encrypted bytes followed by the 16-byte tag:
{ "message_encryption": "e2ee", "key_version": 1, "level": "high", "event": { "type": "backup.failed", "subject": "host:nas" }, "encrypted": { "algorithm": "xchacha20-poly1305", "nonce": "<24 random bytes>", "aad": "<the AAD bytes>", "ciphertext": "<ciphertext and tag>" }}The level, event fields, thread key, and acknowledgement options stay in plaintext so the server can route the alert and recognize repeats. Do not put anything secret in them.
A complete sender in Python, using PyNaCl:
import base64, json, os, urllib.requestfrom nacl.bindings import crypto_aead_xchacha20poly1305_ietf_encrypt
def b64(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def unb64(text: str) -> bytes: return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
_, channel_id, version, key = os.environ["CHANNEL_KEY"].split(":")event = {"type": "backup.failed", "subject": "host:nas"}aad = json.dumps({ "schema": "alert-inbox.message-aad.v1", "channel_id": channel_id, "key_version": int(version), "event_type": event["type"], "event_subject": event["subject"], "thread_key": "",}).encode()nonce = os.urandom(24)content = json.dumps({"title": "Backup failed", "body": "nas: exit code 2"}, ensure_ascii=False, separators=(",", ":")).encode()ciphertext = crypto_aead_xchacha20poly1305_ietf_encrypt(content, aad, nonce, unb64(key))
request = { "message_encryption": "e2ee", "key_version": int(version), "level": "high", "event": event, "encrypted": {"algorithm": "xchacha20-poly1305", "nonce": b64(nonce), "aad": b64(aad), "ciphertext": b64(ciphertext)},}urllib.request.urlopen(urllib.request.Request( os.environ["SEND_URL"], data=json.dumps(request).encode(), headers={"Content-Type": "application/json"}, method="POST"))Use a new random nonce for every alert.
| Field | Requirement |
|---|---|
key_version |
1 or higher, matching the key used |
encrypted.nonce |
Exactly 24 bytes |
encrypted.aad |
1 to 4,096 bytes |
encrypted.ciphertext |
17 to 4,112 bytes: the content plus the 16-byte tag |
An envelope that does not meet these requirements is rejected with invalid_encrypted_message, except content over the size limit below.
Size limit
Section titled “Size limit”The content you encrypt, the JSON object with title, body, and url, may be at most 4,096 bytes. This is the only limit on the content of an encrypted alert:
- The character limits for
alert.title,alert.body, andurldo not apply. The server cannot read the content, and devices show it as decrypted. - The server measures the content as the decoded
ciphertextlength minus the 16-byte tag. - Larger content is rejected with
422 alert_too_long, withfieldset toencrypted.ciphertext,limitto4096, andactualto the content size in bytes.
The limit counts bytes, not characters: an ASCII character takes 1 byte, a Chinese character 3, and an emoji 4. Encode the JSON as UTF-8 without \u escapes, as the example does with ensure_ascii=False, so the most text fits.