加密频道
名称以 private-encrypted- 开头的频道是端到端加密的。你的服务器在发布前加密每条载荷,经授权的浏览器客户端使用从你的授权端点获取的密钥进行解密。Realtime 边缘节点和网络中间方只能看到密文。
生成并存储一个 32 字节的主密钥。主密钥不会出现在 Realtime API 请求中,频道名称前缀即可启用该功能。Bird 无法恢复丢失的密钥,用该密钥加密的载荷在你替换密钥后仍然不可读。
加密频道使用与私有频道相同的端点和签名。授权响应还会以 shared_secret 的形式包含该频道的派生解密密钥。拒绝订阅可以阻止该客户端获取密钥。
生成主密钥
生成 32 个随机字节,将其编码为 base64,并像保存 app secret 一样存储该值:
代码示例
openssl rand -base64 32将其作为 realtime 配置的一部分传给你的服务器 SDK,与 app key 和 secret 放在一起。
发布加密事件
服务器 SDK 检测到频道前缀后,从主密钥派生频道密钥,并在本地加密 JSON 载荷。发布请求包含加密后的信封。
import { BirdClient } from "@messagebird/sdk";
const bird = new BirdClient({
apiKey: process.env.BIRD_API_KEY,
realtime: {
key: process.env.BIRD_REALTIME_KEY,
secret: process.env.BIRD_REALTIME_SECRET,
encryptionMasterKey: process.env.BIRD_REALTIME_MASTER_KEY,
},
});
await bird.realtime.publish("rap_01krdgeqcxet5s7t44vh8rt9mg", {
event: "order.updated",
channels: ["private-encrypted-orders"],
data: { order_id: "ord_123", status: "shipped" },
});from bird import Bird
client = Bird(
realtime_key=os.environ["BIRD_REALTIME_KEY"],
realtime_secret=os.environ["BIRD_REALTIME_SECRET"],
realtime_encryption_master_key=os.environ["BIRD_REALTIME_MASTER_KEY"],
)
client.realtime.publish(
"rap_01krdgeqcxet5s7t44vh8rt9mg",
event="order.updated",
channels=["private-encrypted-orders"],
data={"order_id": "ord_123", "status": "shipped"},
)client, err := bird.NewClient(
option.WithAPIKey(os.Getenv("BIRD_API_KEY")),
option.WithRealtimeCredentials(os.Getenv("BIRD_REALTIME_KEY"), os.Getenv("BIRD_REALTIME_SECRET")),
option.WithRealtimeEncryptionMasterKey(os.Getenv("BIRD_REALTIME_MASTER_KEY")),
)
if err != nil {
log.Fatal(err)
}
_, err = client.Realtime.Publish(context.Background(), "rap_01krdgeqcxet5s7t44vh8rt9mg", bird.RealtimePublishParams{
Event: "order.updated",
Channels: []string{"private-encrypted-orders"},
Data: map[string]any{"order_id": "ord_123", "status": "shipped"},
})$bird = new Bird(getenv('BIRD_API_KEY'), realtime: new RealtimeOptions(
key: getenv('BIRD_REALTIME_KEY'),
secret: getenv('BIRD_REALTIME_SECRET'),
encryptionMasterKey: getenv('BIRD_REALTIME_MASTER_KEY'),
));
$bird->realtime->publish('rap_01krdgeqcxet5s7t44vh8rt9mg', (new RealtimePublish())
->setEvent('order.updated')
->setChannels(['private-encrypted-orders'])
->setData(['order_id' => 'ord_123', 'status' => 'shipped']));一次发布中,加密频道必须是唯一的频道。每个加密频道派生不同的密钥,因此其他频道无法解密同一个加密载荷。SDK 会在本地拒绝这种扇出操作,API 在收到此类请求时返回 E23000。要向多个加密频道发布,请使用批量发布,每个事件对应一个频道。
从你的授权端点返回共享密钥
你的授权端点以与审批私有订阅相同的方式审批加密订阅。使用 SDK 的 authorizeChannel 辅助方法,当频道名称带有加密前缀时,响应会自动包含 shared_secret:
app.post("/bird/auth", async (req, res) => {
const { connection_id, channel_name } = req.body;
const user = getUserFromSession(req);
if (!user || !mayJoin(user, channel_name)) return res.sendStatus(403);
res.json(
await bird.realtime.authorizeChannel({
connectionId: connection_id,
channelName: channel_name,
}),
);
});@app.post("/bird/auth")
def bird_auth():
body = request.get_json()
user = get_user_from_session()
if user is None or not may_join(user, body["channel_name"]):
abort(403)
return client.realtime.authorize_channel(
connection_id=body["connection_id"],
channel_name=body["channel_name"],
)func birdAuth(w http.ResponseWriter, r *http.Request) {
var body struct {
ConnectionID string `json:"connection_id"`
ChannelName string `json:"channel_name"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
user, ok := userFromSession(r)
if !ok || !mayJoin(user, body.ChannelName) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
auth, err := client.Realtime.AuthorizeChannel(bird.RealtimeChannelAuthorizationParams{
ConnectionID: body.ConnectionID,
ChannelName: body.ChannelName,
})
if err != nil {
http.Error(w, "authorization failed", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(auth)
}function birdAuth(string $connectionId, string $channelName, User $user): array
{
if (!mayJoin($user, $channelName)) {
http_response_code(403);
exit;
}
return $bird->realtime->authorizeChannel($connectionId, $channelName);
}SDK 为每个频道派生单独的 shared_secret。因此,对 private-encrypted-orders 的授权无法解密 private-encrypted-invoices。密钥通过你的授权响应传递,不会包含在发送到边缘节点的订阅帧中。
在浏览器中订阅并解密
加密模块使用单独的 @messagebird/realtime/encrypted 入口点。导入它并将其作为客户端的 encryption 选项传入:
代码示例
import { BirdRealtime } from "@messagebird/realtime";
import { encryption } from "@messagebird/realtime/encrypted";
const bird = new BirdRealtime({
appKey: "your-app-key",
region: "us1",
authEndpoint: "/bird/auth",
encryption,
});
const orders = bird.subscribe("private-encrypted-orders");
orders.bind("order.updated", (data) => {
console.log(data); // decrypted: { order_id: "ord_123", status: "shipped" }
});绑定接收到的是明文。未设置 encryption 选项的订阅会立即抛出异常,授权响应中缺少 shared_secret 会导致订阅失败。
目前只有浏览器客户端支持接收加密频道。Swift 和 Kotlin 客户端会拒绝 private-encrypted- 订阅,因为它们未实现解密功能。
轮换主密钥
将新密钥同时部署到所有发布者和授权端点。轮换期间:
- 新发布的消息使用新密钥加密。
- 已订阅的浏览器客户端如果无法解密某个事件,会重新授权一次并获取新的 shared_secret。
- 使用不同主密钥的实例可能短暂地发布某些客户端无法解密的事件,因此请在实例间协调部署。
密钥泄露或丢失时应立即轮换。轮换保护未来的载荷,但无法重新加密之前的事件,也无法撤销旧密钥的副本。
加密频道不支持的功能
- 官方客户端不支持客户端事件。 浏览器 trigger() 在加密频道上会抛出异常,因为客户端不会对客户端到客户端的载荷进行加密。不要从自定义客户端发送明文客户端事件。
- Presence 和加密不能组合使用。presence-encrypted- 前缀不受支持。缓存和加密可以一起使用:private-encrypted-cache- 频道以加密形式存储缓存事件,但密钥轮换后,缓存副本仍使用旧密钥加密,直到下一次发布替换它。
- 频道名称和事件名称不会被加密。 只有载荷会被加密。选择频道名称时,避免泄露你要保护的内容。
- Realtime 边缘节点无法检查载荷。 频道名称和事件名称仍然可见,但载荷保持加密。
后续步骤
相关资源
继续查阅此主题的文档、指南和示例。资源为英文。