已经可以联调
- 多用户账号、原生设备密钥与指纹批准
- v1 广播与 v2 定向响铃加密协议
- 频道角色、细分管理权限与限时邀请
all/future历史密钥策略与成员变更轮换- 一次性离线恢复 JSON、HPKE 持有证明与设备恢复
- Sender 本地加密签名、客户端验签与本地解密
- 三级图片、文字和脚本内联键组成的 E2EE 频道帖子
- Android FCM / Windows WebSocket 多设备远程停铃
- 服务端与客户端 SQLite 密文模型
- 持久游标、断网历史读取与增量补齐
告警在产生处加密,Linux 服务器只保存与转发密文,Android 和 Windows 使用各自设备密钥在本地解密。
先看这里
all/future 历史密钥策略与成员变更轮换all/future 和移除成员的真机密钥边界/admin/ 使用独立身份管理账号、设备信任、会话、运行状态和审计。Admin 可验证当前密码后修改密码,也可向单台已批准 Android 设备发送固定文案的诊断响铃;设备信任批准与频道密钥分发相互独立,普通设备批准不会删除任何旧数据。使用教程
cd D:\code\bullbell
npm run build
powershell -ExecutionPolicy Bypass -File .\scripts\start-local-server.ps1测试数据固定在 data/testing,不会与未来 Linux 生产数据混用。
Windows 使用 http://127.0.0.1:8787;Android 使用电脑当前局域网地址,例如 http://192.168.x.x:8787。
从本机服务日志读取一次性 Admin 初始化代码,打开 http://127.0.0.1:8787/admin/ 创建独立 Admin。客户端提交设备后,在控制台核对指纹并批准。
手机刷新批准状态后填写账号名、显示名称和密码。此后批准其他设备不会删除旧账号、频道或历史。
powershell -ExecutionPolicy Bypass -File .\scripts\stop-local-server.ps1安全模型
服务器仍会看到必要元数据,例如事件时间、频道 ID、密文大小、发送 IP 和设备数量。正文、标题、链接和业务 Payload 必须加密。
加密规范
Android Keystore / Windows CNG 中生成并持久保存,应用只上传公钥,用于解封频道私钥。
每频道独立、支持版本轮换,用于解封每条消息的随机密钥。
每条事件独立随机 32 字节,使用 AES-256-GCM 加密正文。
频道
频道必须由已授权 Android 或 Windows 客户端创建。服务器网页默认不能创建私钥。
客户端生成频道 ID、P-256 密钥对、Webhook Token、Sender 签名密钥和版本号。
一次性导出 .bullbell-channel Sender 配置包,不包含频道私钥。
泄露后启用新 keyVersion;旧私钥只保留用于读取旧历史。
{
"endpoint": "https://<YOUR_DOMAIN>/hook/<CHANNEL_ID>",
"channelId": "<CHANNEL_ID>",
"webhookToken": "<TOKEN>",
"channelPublicKey": "<BASE64URL_PUBLIC_KEY>",
"senderPrivateKey": "<ENCRYPTED_SENDER_KEY>",
"keyVersion": 1
}
多人频道
可修改/删除频道、授予管理员权限、轮换完整频道凭据,并保留 Sender 配置的控制权。
只能执行创建者逐项开启的发送告警、创建/撤销邀请、移除成员和选择历史权限。
可接收、解密、查看告警和提交加密选择回执,不持有 Webhook Token 或外部 Sender 私钥。
@username 全局唯一且可修改,显示名称可以重复。内置头像只同步编号;自定义头像在客户端裁剪缩放后,按每个频道公钥独立加密上传,Server 和备份只能看到头像密文。send_alert 权限的管理员使用本设备独立 Ed25519 密钥签名,私钥留在 Android/Windows 安全存储,不共享 Webhook Token。权限或设备撤销后立即不能再提交新告警。all/future。邀请链接只显示一次。all 为新成员设备封装所有历史版本;future 原子轮换新版本,新成员看不到加入前历史。告警端
Sender 是独立模块,负责模板、本地加密、签名、幂等、断线重试和密文缓存。普通程序不需要自行实现密码学。
bullbell-sender.exe send \
--config sender.secure \
--title "BTC 跌破 70000" \
--body "请立即检查账户" \
--image monitor.png \
--inline-keyboard-file buttons.json按钮是脚本随消息提供的二维 JSON 数组,支持确认并停止、打开 HTTP(S) 链接、复制静态内容和加密选择回执。定向响铃增加 --target-user-ids "<USER_ID>" --non-target normal --ack-policy any-target。
POST http://127.0.0.1:17321/v1/alerts
Authorization: Bearer <LOCAL_RELAY_TOKEN>
Content-Type: application/json
{"title":"磁盘空间不足","body":"只剩 10GB"}协议
{
"version": 1,
"eventId": "<UUID>",
"channelId": "<CHANNEL_ID>",
"keyVersion": 1,
"timestamp": 1786964400,
"nonce": "<BASE64URL_96_BIT_NONCE>",
"wrappedKey": "<HPKE_WRAPPED_DEK>",
"ciphertext": "<AES_GCM_CIPHERTEXT>",
"senderId": "<SENDER_ID>",
"signature": "<ED25519_SIGNATURE>"
}
Server 验证 Token、Sender 签名、时间窗口、Nonce、大小和频率。重试必须复用同一 eventId,以保证幂等。任何字段、AAD、密文或签名被篡改都必须失败。
wrappedKey 为 65 字节 HPKE enc 与 48 字节 DEK 密文拼接;签名覆盖除 signature 外的完整信封。公开互操作向量位于 packages/protocol/test-vectors/v1.json。deliveryMode、targetUserIds、nonTargetBehavior 和 acknowledgementPolicy,全部纳入签名和 AEAD AAD。频道成员仍都可解密消息,只有目标成员响铃。公开向量位于 packages/protocol/test-vectors/v2.json,v1 字节格式保持不变。richContent v2 为最多 4 张图片保存 E2EE 32px 极小图、最长边 1280px 列表图和最大 10 MB 原图,不改变信封格式。App 先显示极小图,在图片接近视口时下载列表密文,点击后才下载原图;消息流缩略图限制在屏幕高度约 36% 且最高 300 逻辑像素,长图只裁切缩略图,原图查看器仍显示完整内容。后台验证 AES-GCM Tag 与 SHA-256 后按显示尺寸解码。客户端只在 32 MB 内存 LRU 保存压缩明文字节,Flutter 已解码图像/纹理缓存限制为 32 MB 和 64 项,256 MB 磁盘缓存仍只有按服务器和设备隔离的密文;旧 v1 图片继续兼容。频道首次打开直接复用内存中的最近消息并锚定在底部,切换动画完成后再同步,不会先闪过旧消息再跳转;到达最早消息后继续下拉并松手即可加载上一页历史。通知
FCM Data Message 只带事件 ID、频道 ID、密钥版本和该设备已计算的级别,不带正文或完整目标列表。
托盘客户端通过 WebSocket 收到事件提示,自动下载密文、验证签名并按本用户重新计算通知级别。
429 与 Retry-After;FCM 的 429、5xx 和临时网络错误会按 Retry-After 或带随机抖动的指数退避最多重试 3 次。收到一条新告警,解锁后查看
alarm 会启动临时前台服务并持续响铃,服务器确认成功后停止;普通和重要告警不会常驻。Android 可从系统文件选择器选择本地音频作为响铃,文件不可读时自动回退内置声音。Windows 使用系统 Alarm 场景并在确认后取消通知。admin-test-alarm 指令,不创建频道历史、不包含正文或频道密钥。状态区分 FCM 接受、设备收到、开始响铃和停止;每设备至少间隔 30 秒,全局每分钟最多 10 次。它用于检查设备通道,不能替代真实 E2EE 频道告警验收。定向确认:any-target 中任一目标确认后,所有目标设备收到 alarm-stop;all-targets 中每个目标用户分别确认,同一用户的其他设备会同步停止。客户端仅在 eventId 与当前响铃一致时执行远程停铃。
客户端导航:底部固定为“总览、频道、告警中心、设置”。频道页使用明亮白色列表展示角色、最后告警、未读和待确认状态;频道资料的“频道”区域直接提供成员、邀请、新成员历史权限和管理员权限入口,并集中显示加密状态及仅创建者可见的 Token/Sender 管理。Token 原值只在创建或重置时显示一次,之后仅显示末尾 6 位与轮换时间。
客户端设置:账号资料位于设置页顶部,独立显示头像、显示名称、@username 和修改密码入口,不归入“常规”。“常规”负责外观、固定配色、最近任务卡片、通知声音和响铃音频;“权限管理”集中管理通知、全屏响铃、后台电池、设备批准和离线恢复;“使用指南”按主题打开本页。后台隐藏只改变最近任务卡片,不停止 FCM、通知、响铃或后台任务。Android 不允许应用静默覆盖用户已经确认的通道开关,因此悬浮、振动和锁屏显示必须由用户在跳转后的系统页面修改。
告警中心
设备本地解密后的标题与正文
服务器返回的始终是频道密文分页
完整历史移入频道内部。告警中心跨频道提供待处理、指向我、全部、已确认、收藏、频道筛选和本地全文搜索,搜索词不会上传服务器。服务端事件游标在 SQLite 重启后仍然有效。客户端按服务器 URL 与设备 ID 隔离缓存;Android 进入频道后优先读取已经验签解密的明文记录,Windows 读取密文并在内存解密,再在后台同步最新密文。每个频道在本机只保留最近 1000 条,超过本地边界的旧记录按服务端游标联网读取。
密钥生命周期
/admin/ 核对指纹并批准。此操作不删除旧数据,也不生成或读取频道私钥。部署
cd /opt/bullbell/deploy/linux
cp .env.example .env
# 已有 Nginx/Caddy 时只绑定 127.0.0.1:8787
docker compose -f compose.existing-proxy.yaml up -d --build
# 后续正式更新只部署明确标签
./scripts/deploy.sh v0.22.0
8787 不直接开放公网。已有站点时使用独立的现有反向代理 Compose;没有现有代理时使用自带 Caddy 的 compose.yaml。Server 0.13.0 使用 bullbell.sqlite 保存 Admin 摘要与审计、设备注册申请、多用户、设备信任、频道成员/邀请、应用内发送公钥、密钥包、每频道最近 10,000 条密文事件、附件密文和频道头像密文。两套 Compose 均包含健康检查、384 MB 内存限制和 10 MB × 3 日志轮换。客户端正式地址填写 https://<YOUR_DOMAIN>,不要附加 /api。
运维
加密范围只包括 Linux 上的 BullBell 数据卷和 BullBell 备份文件。本机测试目录是 D:\code\bullbell\data\testing,文档不会启用 BitLocker、修改分区或加密整个 D 盘。迁移验证完成前保留的旧 JSON 也必须作为敏感备份保护。
backup.sh 使用 SQLite 在线备份生成 AES-256/PBKDF2 加密快照,无需停止服务;monitor.sh 检查容器、公网 HTTPS、磁盘和内存;systemd timer 默认每 5 分钟监控、每天备份。服务器只保留当前和上一 Server 镜像、APK 与 Windows 包,GitHub Private Releases 可长期归档更多版本。发布
# 当前开发代码
Server 0.13.0
Client 0.22.0+56
# 当前网站正式包
Client 0.21.0+55
vX.Y.Z Git 标签,Linux 部署脚本拒绝未标记代码。开发规范
Schema、JCS、HPKE、AES-GCM、Ed25519 已完成。
Keystore、CNG、指纹批准、离线恢复和密钥轮换已完成。
安全配置、中继、密文队列与单文件打包已完成。
SQLite WAL、防重放、幂等和持久游标已完成。
按频道分页、断网历史和活动告警前台服务已完成。
生产 FCM、红米锁屏省电、HTTPS 与备份恢复待实测。
AGENTS.md 要求所有后续实现先完整阅读 docs/PROJECT_SPEC.md,架构变化必须同步更新本网页。完成标准
换一个关键词,或清除顶部搜索框。