已经可以联调
- 多用户账号、原生设备密钥与指纹批准
- v1 广播与 v2 定向响铃加密协议
- 频道角色、细分管理权限与限时邀请
all/future历史密钥策略与成员变更轮换- 一次性离线恢复 JSON、HPKE 持有证明与设备恢复
- Sender 本地加密签名、客户端验签与本地解密
- 三级图片、文字和脚本内联键组成的 E2EE 频道帖子
- Android/Windows WebSocket 与 Android FCM 备用通道
- 服务端与客户端 SQLite 密文模型
- 持久游标、断网历史读取与增量补齐
告警在产生处加密,Linux 服务器只保存与转发密文,Android 和 Windows 使用各自设备密钥在本地解密。
先看这里
all/future 历史密钥策略与成员变更轮换all/future 和移除成员的真机密钥边界/admin/ 使用独立身份管理账号、设备信任、会话、运行状态和审计。Admin 可验证当前密码后修改密码,也可向单台已批准设备发送固定文案的诊断响铃:Android 走 FCM,在线 Windows 走已认证 WebSocket。设备信任批准与频道密钥分发相互独立,普通设备批准不会删除任何旧数据。使用教程
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"}开发接口
SDK 0.1.0 直接运行在发送脚本内,不需要启动 Sender.exe 或本机中继。库在本机加密、签名后通过 HTTPS 发送,Server 仍看不到标题正文。
登录有权限的 GitHub 账号,从项目私有 Releases 下载 wheel(Python 3.11+)或 tgz(Node.js 22+),校验 SHA256SUMS 后安装。完整安装、配置导入、三种通知级别、定向响铃、内联按钮与重试教程在私有仓库 docs/sdk/README.md。SDK 不发布公共 PyPI/npm,勿安装陌生同名包。
在私有仓库的 sdk-v0.1.0 Release 下载对应安装包和 SHA256SUMS.txt。SDK 下载需要 GitHub 读取权限,日常发送不访问 GitHub。不要把 GitHub Token 写在安装命令或发送脚本中。
# Python:在发送脚本使用的 Python 环境安装
python -m pip install ./bullbell_sdk-0.1.0-py3-none-any.whl
# JavaScript / TypeScript:在业务项目目录安装
npm install ./bullbell-sdk-0.1.0.tgz --save-exact
设置环境变量 BULLBELL_CONFIG 为现有 sender.secure 路径。Windows 必须由导入配置的同一用户运行;Linux 口令配置还需设置 BULLBELL_SENDER_PASSPHRASE。没有安全配置时,Python 使用 protect_config、JavaScript 使用 protectSenderConfig 导入客户端一次性 Sender JSON,无需 Sender 工具。凭据只保存在受信任的发送机器上,不随 SDK 分发。
normal 为普通通知,important 为重要通知,alarm 为响铃。不传级别时使用 Sender 配置的默认值,同一频道可发送三种级别。
# Python:BULLBELL_CONFIG 指向已有受保护 Sender 配置
import os
from bullbell import BullBell
bell = BullBell(config_file=os.environ["BULLBELL_CONFIG"])
result = bell.send(title="服务异常", body="请检查服务器", urgency="alarm")
// JavaScript / TypeScript(Node.js)
import { BullBell } from "@bullbell/sdk";
const bell = await BullBell.fromFile(process.env.BULLBELL_CONFIG);
const result = await bell.send({title:"服务异常", body:"请检查服务器", urgency:"alarm"});
目标使用频道成员 UUID,不是用户名或设备 ID。所有有权成员仍可查看消息;非目标成员按 silent、normal 或 important 处理。
# Python
result = bell.send(
title="值班告警", body="请值班人员处理", urgency="alarm",
target_user_ids=["<USER_UUID>"],
non_target_behavior="normal", acknowledgement_policy="any-target",
)
// JavaScript / TypeScript
const result = await bell.send({
title: "值班告警", body: "请值班人员处理", urgency: "alarm",
targetUserIds: ["<USER_UUID>"],
nonTargetBehavior: "normal", acknowledgementPolicy: "any-target",
});
any-target 表示任一目标确认后停止全部目标;all-targets 要求每个目标用户分别确认。目标列表和级别是服务器可见的路由信息,正文仍为密文。
[
[{"type":"acknowledge","label":"确认并停止"}],
[{"type":"url","label":"打开监控","url":"https://monitor.example.invalid/"}],
[{"type":"copy","label":"复制节点","value":"db-02"}],
[{"type":"choice","label":"已接单","choiceId":"take","value":"accepted"}]
]
Python 将此数组作为 inline_keyboard=buttons 传入;JavaScript 使用 richContent: {version:1, inlineKeyboard:buttons}。最多 8 行、每行 4 个、总计 20 个按钮。按钮内容随正文加密,选择回执不是服务端代为执行任意 HTTP 回调。
默认最多尝试 3 次,遇到网络错误、408、429 或 5xx 时遵循 Retry-After 并指数退避加抖动;401/403 等永久错误不重试,也不跟随重定向。Python 的 timeout 单位是秒,JavaScript 的 timeoutMs 单位是毫秒,默认每次请求 15 秒。成功结果含 eventId/status/attempts/response,只证明服务端接受,不证明设备已响铃。
# Python:需要业务持久队列时,先保存 prepare 返回的密文
from bullbell import DeliveryError
envelope = bell.prepare(title="服务异常", body="请检查服务器", urgency="alarm")
try:
result = bell.submit(envelope)
except DeliveryError as error:
print(error.status, error.retryable)
# 稍后仅重试 bell.submit(error.envelope),不要重新 send 创建事件
JavaScript 对应 await bell.prepare(input)、await bell.submit(envelope) 和 DeliveryError。SDK 不自动写持久队列;业务只应保存密文信封。服务端有前后 300 秒的时间窗口,过旧或密钥已轮换的事件不能悄悄改时间戳或换 eventId 补发。频道完整轮换后旧配置失效,需更新发送凭据。
prepare/submit 重放同一份密文,不能反复新建事件。服务端接受不等于设备已经响铃。SDK 配置、口令和签名私钥不得上传 GitHub 或包含在安装包中。协议
{
"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、仅长连接、FCM 优先/长连接备用、长连接优先/FCM 备用。每台设备单独设置,旧设备默认长连接优先。
托盘客户端通过 WebSocket 收到事件提示,自动下载密文、验证签名并按本用户重新计算通知级别。本轮 Android 勿扰不改变 Windows 通知行为。
429 与 Retry-After;FCM 的 429、5xx 和临时网络错误会按 Retry-After 或带随机抖动的指数退避最多重试 3 次。收到一条新告警,解锁后查看
alarm 使用临时前台服务持续响铃,可选择本地音频,文件不可读时回退内置声音。Windows 使用应用控制的循环系统声音与独立白色窗口;1.1.2 起小窗位于主屏右下角,距工作区边缘 16 像素,不主动抢输入焦点、不打开主界面,也不新增任务栏按钮。“确认并停止”或关闭响铃窗口先停止本机声音,再发送频道确认或 Admin 回执。旧事件的停止指令不会误停新响铃。普通/重要告警仍走系统通知。admin-test-alarm 指令,向当前在线的 Windows 设备通过已认证 WebSocket 发送同类指令。它不创建频道历史、不包含正文或频道密钥。状态区分投递通道接受、设备收到、开始响铃和停止;每设备至少间隔 30 秒,全局每分钟最多 10 次。定向确认:any-target 中任一目标确认后,所有目标设备收到 alarm-stop;all-targets 中每个目标用户分别确认,同一用户的其他设备会同步停止。客户端仅在 eventId 与当前响铃一致时执行远程停铃。
客户端导航:底部固定为“总览、频道、告警中心、设置”。频道页使用明亮白色列表展示角色、最后告警、未读和待确认状态;频道资料的“频道”区域直接提供成员、邀请、新成员历史权限和管理员权限入口,并集中显示加密状态及仅创建者可见的 Token/Sender 管理。Token 原值只在创建或重置时显示一次,之后仅显示末尾 6 位与轮换时间。
客户端设置:账号资料位于设置页顶部,独立显示头像、显示名称、@username 和修改密码入口,不归入“常规”。“常规”负责外观、固定配色、最近任务卡片、通知声音和响铃音频;“权限管理”的“系统权限”显示全部七项用户可检查权限,随后管理设备批准和离线恢复;“使用指南”按主题打开本页。Android 的“安装应用更新”只影响下载后安装 APK,不影响日常收警。后台隐藏只改变最近任务卡片,不停止 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.15.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 可长期归档更多版本。Windows 1.2.1 低内存试用版:关闭主窗口先隐藏再释放界面内存;托盘快速重开会等待旧界面退出并合并重复点击。启动复用后台会话,先显示本地缓存,再通过进度条提示联网同步,不再每次打开都刷新令牌或注册设备密钥。登录失效仍要求重新登录,断网仍明确提示。此优化不让整个界面长期驻留后台,完全退出后的冷启动成本仍存在。
发布
# 当前开发代码
Server 0.15.0
Client 1.1.0+64
# 当前网站正式包
Client 1.1.0+64
vX.Y.Z Git 标签,Linux 部署脚本拒绝未标记代码。开发规范
Schema、JCS、HPKE、AES-GCM、Ed25519 已完成。
Keystore、CNG、指纹批准、离线恢复和密钥轮换已完成。
安全配置、中继、密文队列与单文件打包已完成。
SQLite WAL、防重放、幂等和持久游标已完成。
按频道分页、断网历史和活动告警前台服务已完成。
生产 FCM、红米锁屏省电、HTTPS 与备份恢复待实测。
AGENTS.md 要求所有后续实现先完整阅读 docs/PROJECT_SPEC.md,架构变化必须同步更新本网页。完成标准
换一个关键词,或清除顶部搜索框。