加密提醒
开启端到端加密后,发送端在发送前加密提醒的标题、正文和链接,只有持有频道密钥的设备才能读取。Gregor 服务器保存并投递密文,但永远看不到内容。
服务器不保存、不分发、不验证也不恢复频道密钥。你需要自己把密钥复制给每个发送端和设备,没有密钥的设备无法读取这些提醒。
- 在频道页打开频道的操作菜单,选择频道设置。
- 在端到端加密下打开要求加密消息并保存。此后频道会以
encryption_required拒绝明文提醒。 - 重新打开频道设置,选择管理频道密钥,再选择生成密钥。
- 选择复制密钥,把密钥交给发送端和其他需要读取提醒的设备。在这些设备上打开管理频道密钥,选择导入密钥。
不要求加密的频道同时接受明文和加密提醒。
导出的密钥格式如下:
ck1:<channel_id>:<key_version>:<key><key> 是 32 个随机字节,使用不带填充的 base64url 编码。请把整个字符串当作机密保管。
轮换密钥会生成版本号加一的新密钥。设备会保留旧版本,因此之前加密的提醒仍然可以读取。新密钥需要再次复制给发送端和设备,Gregor 不会替你分发。
谁会收到加密提醒
Section titled “谁会收到加密提醒”频道设置控制加密提醒的投递和显示:
- 加密消息投递:所有接收者、仅选中的接收者或排除选中的接收者。它控制加密提醒发给谁;服务器无法知道谁真正持有密钥。
- 本机没有密钥时:显示占位消息表示收到了一条无法读取的加密提醒;从普通视图隐藏则把它隐藏起来。
要求加密的频道不支持心跳,因为心跳提醒由服务器生成,服务器无法加密。
发送加密提醒
Section titled “发送加密提醒”命令行工具会替你完成下面所有步骤:设置 CHANNEL_KEY 后照常发送即可。要自己编写发送端,请按以下步骤操作。
用 XChaCha20-Poly1305 加密一个包含提醒内容的 JSON 对象:
{ "title": "备份失败", "body": "nas:退出码 2", "url": "https://nas.example.com" }附加认证数据(AAD)是下面这个 JSON 对象的 UTF-8 编码。它的值必须与请求外层的字段一致,没有发送的字段用空字符串;AAD 不一致的提醒会被设备拒绝:
{ "schema": "alert-inbox.message-aad.v1", "channel_id": "<channel_id>", "key_version": 1, "event_type": "backup.failed", "event_subject": "host:nas", "thread_key": ""}用加密结果代替 alert 发送,二进制值都使用不带填充的 base64url 编码。ciphertext 是密文字节加上 16 字节的认证标签:
{ "message_encryption": "e2ee", "key_version": 1, "level": "high", "event": { "type": "backup.failed", "subject": "host:nas" }, "encrypted": { "algorithm": "xchacha20-poly1305", "nonce": "<24 个随机字节>", "aad": "<AAD 字节>", "ciphertext": "<密文和认证标签>" }}级别、事件字段、thread key 和确认选项保持明文,这样服务器才能路由提醒并识别重复事件。不要在这些字段里放机密信息。
一个使用 PyNaCl 的完整 Python 发送端:
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": "备份失败", "body": "nas:退出码 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"))每条提醒都要使用新的随机 nonce。
| 字段 | 要求 |
|---|---|
key_version |
1 或更大,与所用密钥一致 |
encrypted.nonce |
正好 24 字节 |
encrypted.aad |
1 到 4096 字节 |
encrypted.ciphertext |
17 到 4112 字节:内容加 16 字节的认证标签 |
不符合这些要求的信封会以 invalid_encrypted_message 拒绝;内容超过下面的大小限制时除外。
加密的内容,即包含 title、body 和 url 的 JSON 对象,最多 4096 字节。这是加密提醒内容的唯一限制:
alert.title、alert.body和url的字符数限制不适用。服务器读不到内容,设备按解密结果原样显示。- 服务器按解码后的
ciphertext长度减去 16 字节认证标签计算内容大小。 - 超过时以
422 alert_too_long拒绝,其中field为encrypted.ciphertext,limit为4096,actual为内容的字节数。
这个限制按字节而不是字符计算:一个 ASCII 字符占 1 字节,一个汉字占 3 字节,一个表情符号占 4 字节。把 JSON 编码为 UTF-8,不要用 \u 转义(示例中的 ensure_ascii=False),才能放下最多的文字。