B BullBellDOCS / 目标规范 v3
SELF-HOSTED · END-TO-END ENCRYPTED

一套告警协议,
两端安全抵达。

告警在产生处加密,Linux 服务器只保存与转发密文,Android 和 Windows 使用各自设备密钥在本地解密。

01告警程序产生标题与正文
02Sender本地加密与签名
03Server只接触密文
04设备验证并本地解密
00

先看这里

当前测试版与目标架构

当前原型

已经可以联调

  • 多用户账号、原生设备密钥与指纹批准
  • v1 广播与 v2 定向响铃加密协议
  • 频道角色、细分管理权限与限时邀请
  • all/future 历史密钥策略与成员变更轮换
  • 一次性离线恢复 JSON、HPKE 持有证明与设备恢复
  • Sender 本地加密签名、客户端验签与本地解密
  • 三级图片、文字和脚本内联键组成的 E2EE 频道帖子
  • Android FCM / Windows WebSocket 多设备远程停铃
  • 服务端与客户端 SQLite 密文模型
  • 持久游标、断网历史读取与增量补齐
下一阶段

仍需实测

  • 多成员/多设备定向响铃与远程停铃
  • all/future 和移除成员的真机密钥边界
  • 红米长时间后台与省电策略
  • Linux HTTPS、备份恢复和轮换演练
Admin 控制面:/admin/ 使用独立身份管理账号、设备信任、会话、运行状态和审计。Admin 可验证当前密码后修改密码,也可向单台已批准 Android 设备发送固定文案的诊断响铃;设备信任批准与频道密钥分发相互独立,普通设备批准不会删除任何旧数据。
当前边界:开发代码为 Server 0.13.0、客户端 0.22.0+56、Sender 0.9.0 与 Protocol 0.8.0;网站当前正式下载包仍为 0.21.0,直到新版本完成签名构建与真机验收。Admin 无法查看告警正文或频道私钥。当前离线恢复仍是单一服务器级结构,尚未支持每个成员独立恢复。
01

使用教程

从本机测试开始

1

启动测试服务器

cd D:\code\bullbell
npm run build
powershell -ExecutionPolicy Bypass -File .\scripts\start-local-server.ps1

测试数据固定在 data/testing,不会与未来 Linux 生产数据混用。

2

连接客户端

Windows 使用 http://127.0.0.1:8787;Android 使用电脑当前局域网地址,例如 http://192.168.x.x:8787

3

初始化 Admin 并批准手机

从本机服务日志读取一次性 Admin 初始化代码,打开 http://127.0.0.1:8787/admin/ 创建独立 Admin。客户端提交设备后,在控制台核对指纹并批准。

4

创建 BullBell 账号

手机刷新批准状态后填写账号名、显示名称和密码。此后批准其他设备不会删除旧账号、频道或历史。

5

停止测试服务器

powershell -ExecutionPolicy Bypass -File .\scripts\stop-local-server.ps1
02

安全模型

谁能够看到明文

允许看到明文

  • 产生告警的原始程序
  • 同机 BullBell Sender
  • 已授权 Android/Windows 设备

只能看到密文

  • BullBell Server 与 SQLite
  • 备份、Caddy、FCM、WebSocket
  • 文档站与普通浏览器

服务器仍会看到必要元数据,例如事件时间、频道 ID、密文大小、发送 IP 和设备数量。正文、标题、链接和业务 Payload 必须加密。

03

加密规范

设备、频道与消息三级密钥

DEVICE

设备私钥

Android Keystore / Windows CNG 中生成并持久保存,应用只上传公钥,用于解封频道私钥。

CHANNEL

频道私钥

每频道独立、支持版本轮换,用于解封每条消息的随机密钥。

EVENT

消息密钥

每条事件独立随机 32 字节,使用 AES-256-GCM 加密正文。

内容加密AES-256-GCM
密钥封装HPKE / P-256
请求签名Ed25519
规范化RFC 8785 JCS
登录密码不是加密密钥。修改账号密码不会重新加密历史;数据恢复依靠独立的离线恢复密钥。修改成功后旧访问令牌立即失效,全部旧刷新令牌撤销,其他设备的推送绑定和 Windows 长连接也会失效;当前设备取得新令牌并保持登录。访问令牌约 15 分钟,刷新令牌按设备轮换;客户端会合并同一批并发刷新,避免多个 401 相互撤销登录态。
04

频道

创建、导出与轮换

频道必须由已授权 Android 或 Windows 客户端创建。服务器网页默认不能创建私钥。

创建

客户端生成频道 ID、P-256 密钥对、Webhook Token、Sender 签名密钥和版本号。

导出

一次性导出 .bullbell-channel Sender 配置包,不包含频道私钥。

轮换

泄露后启用新 keyVersion;旧私钥只保留用于读取旧历史。

轮换是完整凭据替换:在频道页点击“轮换密钥”后,客户端会为全部已批准设备和恢复公钥生成新版本包,同时创建新 Token 与新 Sender。旧 Sender 配置立即失效;新配置只显示一次,必须先更新发送端再关闭。
{
  "endpoint": "https://<YOUR_DOMAIN>/hook/<CHANNEL_ID>",
  "channelId": "<CHANNEL_ID>",
  "webhookToken": "<TOKEN>",
  "channelPublicKey": "<BASE64URL_PUBLIC_KEY>",
  "senderPrivateKey": "<ENCRYPTED_SENDER_KEY>",
  "keyVersion": 1
}
04A

多人频道

成员、邀请与历史权限

创建者

可修改/删除频道、授予管理员权限、轮换完整频道凭据,并保留 Sender 配置的控制权。

管理员

只能执行创建者逐项开启的发送告警、创建/撤销邀请、移除成员和选择历史权限。

普通成员

可接收、解密、查看告警和提交加密选择回执,不持有 Webhook Token 或外部 Sender 私钥。

账号资料与头像:@username 全局唯一且可修改,显示名称可以重复。内置头像只同步编号;自定义头像在客户端裁剪缩放后,按每个频道公钥独立加密上传,Server 和备份只能看到头像密文。
应用内发送:send_alert 权限的管理员使用本设备独立 Ed25519 密钥签名,私钥留在 Android/Windows 安全存储,不共享 Webhook Token。权限或设备撤销后立即不能再提交新告警。
  1. 创建邀请在频道成员页设置角色、权限、有效期、使用次数和 all/future。邀请链接只显示一次。
  2. 接受邀请新用户可从登录页使用邀请注册;已有用户在应用内粘贴链接。接受后先处于待批准。
  3. 批准并分发密钥all 为新成员设备封装所有历史版本;future 原子轮换新版本,新成员看不到加入前历史。
  4. 移除成员客户端为剩余成员设备生成新版本密钥包,服务端与成员移除原子提交。
为什么多人能解密同一条消息?正文只用随机 DEK 加密一次,频道私钥的同一版本再分别 HPKE 封装给每台成员设备。每台设备都用自己的 Keystore/CNG 私钥解开它的那份频道密钥包。
05

告警端

BullBell Sender

Sender 是独立模块,负责模板、本地加密、签名、幂等、断线重试和密文缓存。普通程序不需要自行实现密码学。

当前 0.9.0:Windows 使用当前用户 DPAPI;Linux 优先使用 Secret Service,无密钥环时使用 scrypt + AES-256-GCM。图片在 Sender 本地生成三级内容并使用独立 AES-256-GCM 文件密钥加密,断线队列仍只保存已签名消息密文。

命令行

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"}
严格 E2EE 限制:Server 自 0.5.0 起会拒绝普通明文 Webhook。只支持明文 Webhook 的第三方平台必须先经过用户控制的 Sender/中继,在本机完成加密后再发送。
06

协议

加密信封与防重放

{
  "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、密文或签名被篡改都必须失败。

v1 已固化:AAD 是 Header 八个字段的 JCS UTF-8;wrappedKey 为 65 字节 HPKE enc 与 48 字节 DEK 密文拼接;签名覆盖除 signature 外的完整信封。公开互操作向量位于 packages/protocol/test-vectors/v1.json
v2 定向路由:新增 deliveryModetargetUserIdsnonTargetBehavioracknowledgementPolicy,全部纳入签名和 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 图片继续兼容。频道首次打开直接复用内存中的最近消息并锚定在底部,切换动画完成后再同步,不会先闪过旧消息再跳转;到达最早消息后继续下拉并松手即可加载上一页历史。
07

通知

Android FCM 与 Windows WebSocket

A

Android

FCM Data Message 只带事件 ID、频道 ID、密钥版本和该设备已计算的级别,不带正文或完整目标列表。

W

Windows

托盘客户端通过 WebSocket 收到事件提示,自动下载密文、验证签名并按本用户重新计算通知级别。

发送保护:每个频道和每个 Webhook Token/应用内 Sender 都有独立令牌桶。超限返回 429Retry-After;FCM 的 4295xx 和临时网络错误会按 Retry-After 或带随机抖动的指数退避最多重试 3 次。
锁屏默认展示BullBell

收到一条新告警,解锁后查看

响铃生命周期:Android 只有 alarm 会启动临时前台服务并持续响铃,服务器确认成功后停止;普通和重要告警不会常驻。Android 可从系统文件选择器选择本地音频作为响铃,文件不可读时自动回退内置声音。Windows 使用系统 Alarm 场景并在确认后取消通知。
Admin 单设备测试:控制台只向已批准且已注册 FCM 的 Android 设备发送固定 admin-test-alarm 指令,不创建频道历史、不包含正文或频道密钥。状态区分 FCM 接受、设备收到、开始响铃和停止;每设备至少间隔 30 秒,全局每分钟最多 10 次。它用于检查设备通道,不能替代真实 E2EE 频道告警验收。

定向确认:any-target 中任一目标确认后,所有目标设备收到 alarm-stopall-targets 中每个目标用户分别确认,同一用户的其他设备会同步停止。客户端仅在 eventId 与当前响铃一致时执行远程停铃。

全屏响铃权限:Android 14 必须允许 BullBell 使用全屏通知;HyperOS 还需允许“锁屏显示”和“后台弹出界面”。权限页实时读取系统通知、全屏通知 AppOps 和电池优化状态,从系统设置返回后自动刷新,不以 Manifest 声明代替实际授权。权限齐全时响铃会自动亮屏并覆盖锁屏,停止后返回桌面。关闭全屏权限后系统仍可亮屏并持续响铃,但可能阻止响铃页面自动出现。

客户端导航:底部固定为“总览、频道、告警中心、设置”。频道页使用明亮白色列表展示角色、最后告警、未读和待确认状态;频道资料的“频道”区域直接提供成员、邀请、新成员历史权限和管理员权限入口,并集中显示加密状态及仅创建者可见的 Token/Sender 管理。Token 原值只在创建或重置时显示一次,之后仅显示末尾 6 位与轮换时间。

客户端设置:账号资料位于设置页顶部,独立显示头像、显示名称、@username 和修改密码入口,不归入“常规”。“常规”负责外观、固定配色、最近任务卡片、通知声音和响铃音频;“权限管理”集中管理通知、全屏响铃、后台电池、设备批准和离线恢复;“使用指南”按主题打开本页。后台隐藏只改变最近任务卡片,不停止 FCM、通知、响铃或后台任务。Android 不允许应用静默覆盖用户已经确认的通道开关,因此悬浮、振动和锁屏显示必须由用户在跳转后的系统页面修改。

08

告警中心

频道历史与跨频道处理

BTC 告警 / 历史记录
19:42

设备本地解密后的标题与正文

18:10

服务器返回的始终是频道密文分页

完整历史移入频道内部。告警中心跨频道提供待处理、指向我、全部、已确认、收藏、频道筛选和本地全文搜索,搜索词不会上传服务器。服务端事件游标在 SQLite 重启后仍然有效。客户端按服务器 URL 与设备 ID 隔离缓存;Android 进入频道后优先读取已经验签解密的明文记录,Windows 读取密文并在内存解密,再在后台同步最新密文。每个频道在本机只保留最近 1000 条,超过本地边界的旧记录按服务端游标联网读取。

Android 本机保存明文:标题、正文、链接和 Payload 在首次成功验签解密后写入应用私有 SQLite;系统备份已关闭,但 root、调试取证或设备失陷仍可能读取。Windows 仍只落密文,BullBell Server、FCM 和 WebSocket 始终不能看到明文。退出登录默认保留本机历史,设置页可显式清除当前范围。
09

密钥生命周期

设备批准、撤销与恢复

  1. 新设备生成密钥私钥留在 Keystore/CNG,公钥注册到服务器并展示 SHA-256 指纹。
  2. Admin 批准设备信任/admin/ 核对指纹并批准。此操作不删除旧数据,也不生成或读取频道私钥。
  3. 按频道补齐密钥已有设备在本地解封频道私钥并重新封装给新设备;控制台独立显示齐全或缺失。
  4. 没有现有设备时恢复已由 Admin 批准的设备使用离线恢复 JSON 解开 HPKE 挑战并补齐频道密钥;恢复文件不能自行授予设备信任。
恢复密钥必须离线保存。首次初始化或设置页创建后,先复制并保存到离线加密介质,再确认“我已离线保存”。确认后客户端删除暂存副本,服务器永远无法再次提供私钥。所有设备和恢复密钥同时丢失后,历史将永久无法解密。
恢复文件疑似泄露时:在一台已批准设备的设置页点击“轮换”,立即保存新的恢复 JSON。服务器原子替换全部恢复包并废止旧恢复公钥;提交成功后旧文件不能再恢复设备。
10

部署

2 GB Linux 服务器

CaddyHTTPS / 80 / 443
Node.jsAPI / FCM / WebSocket
SQLite密文 / WAL / 游标
Docker Volume独立生产数据
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

11

运维

备份不会加密整个 D 盘

加密范围只包括 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 可长期归档更多版本。

需要保护

  • SQLite/数据卷备份
  • Firebase 服务账号
  • APK 签名密钥
  • Sender 配置与恢复密钥

绝不写入文档

  • 真实 Token 和密码
  • 设备/频道私钥
  • 恢复密钥
  • 生产内部地址与凭据
12

发布

版本、签名与更新

# 当前开发代码
Server 0.13.0
Client 0.22.0+56

# 当前网站正式包
Client 0.21.0+55
  • 整套正式发布使用 vX.Y.Z Git 标签,Linux 部署脚本拒绝未标记代码。
  • Android 构建号每次发布递增,并始终使用同一份受保护签名密钥;正式脚本交互读取密码,配置缺失时不会回退到 Debug 签名。
  • Windows 必须分发完整 Release 目录或安装器,不能只复制 EXE。
  • 发布清单保存版本、构建号、下载 URL、文件大小、APK SHA-256 和发布证书 SHA-256。
Android 应用内更新:登录后后台检查一次,仅轻提示新版本;也可从“设置 → 常规 → 应用更新”手动检查。下载后先验证大小和 SHA-256,再由 Android 验证应用 ID、更高构建号、当前安装签名与清单签名。首次需允许 BullBell 安装未知应用,每次仍要在系统安装页确认“更新”,不会静默安装。
13

开发规范

必须按依赖顺序实施

  1. 01 ✓
    协议与测试向量

    Schema、JCS、HPKE、AES-GCM、Ed25519 已完成。

  2. 02 ✓
    设备与恢复

    Keystore、CNG、指纹批准、离线恢复和密钥轮换已完成。

  3. 03 ✓
    频道与 Sender

    安全配置、中继、密文队列与单文件打包已完成。

  4. 04 ✓
    服务端密文模型

    SQLite WAL、防重放、幂等和持久游标已完成。

  5. 05 ✓
    客户端同步与通知

    按频道分页、断网历史和活动告警前台服务已完成。

  6. 06 ◐
    生产演练

    生产 FCM、红米锁屏省电、HTTPS 与备份恢复待实测。

开发前必读:仓库根目录 AGENTS.md 要求所有后续实现先完整阅读 docs/PROJECT_SPEC.md,架构变化必须同步更新本网页。
14

完成标准

不通过这些测试就不算 E2EE