一个文件就是一个加密聊天室。 不用注册、不用装 App:打开网页点一下,得到一个 index.html,发给朋友,大家打开就能聊。断网也能创建房间;装了 Node.js 的电脑上运行同一个文件,它就是服务器。
👉 马上创建房间(也可使用 Vercel 入口 或 备用入口)
- 网站:https://hr50.freedom8964.com/
- WSS 中继源:
wss://hr50.freedom8964.com
网站与中继部署在同一个 Cloudflare Worker,网页默认使用上述中继源。将中继源填入「中继服务器」即可使用;客户端会自动添加房间路径 /hr/<roomId>。
此实例的部署配置为 wrangler.hr50.jsonc,更新命令:npm ci && npm run build && npx wrangler deploy --config wrangler.hr50.jsonc。
整个项目就是一个 index.html:怎么打开它,它就扮演什么角色。
flowchart TD
F["index.html(一个文件)<br/>同时是网页和服务器"]
F --> A["空白文件 · 浏览器<br/>创建器(可离线)"]
F --> B["房间文件 · 浏览器<br/>加密聊天室"]
F --> C["node index.html<br/>中继服务器"]
A --> G["生成房间文件<br/>新钥匙,可设口令和标题"]
G -- 发给朋友 --> B
B -. "↻ 每个房间文件也能再生新房间" .-> G
B <--> R["中继(只转发密文)<br/>wss:// 经典 · bc+ / nostr+ / mqtt+ 去中心化"]
C --> R
classDef file fill:#F1EFE8,stroke:#5F5E5A,color:#2C2C2A
classDef browser fill:#EEEDFE,stroke:#534AB7,color:#26215C
classDef relay fill:#E1F5EE,stroke:#0F6E56,color:#04342C
class F file
class A,B,G browser
class C,R relay
- 浏览器打开空白文件:创建器。网站本身就是它,底部可以「下载离线版」,以后断网也能创建房间。
- 浏览器打开房间文件:加密聊天室。文件里还带着创建器,可以离线生成钥匙不同的新房间。
node index.html:中继服务器,零依赖。空白文件运行时还会提供创建页面;房间文件运行时不公开自己,以免泄露钥匙。- 聊天时消息经过中继,中继只看得到密文。
1. 创建房间 打开创建页面,直接点「创建房间」。可选:
- 房间标题:最多 60 字,显示在浏览器标签页和页面顶部;不填就是「聚众加密聊天房间」。
- 口令:相当于门上再加一把锁。建议点「随机生成口令」:会生成一串好念好抄的口令(如
K7QM-3XRP-9DTA-HWVE-N4BC),直接显示出来,点「复制口令」即可复制;房间建好后口令仍会显示在结果里。
2. 发给朋友 下载 index.html 发给朋友,或复制房间链接。设了口令的话,口令请换一种方式告诉对方(比如文件用微信发,口令打电话说)。
3. 核对安全码 进房后屏幕上会显示一串安全码。请用电话或当面互相念一遍,一致才说明没有人冒充。有人进出、换中继之后,再核对一次。
💡 每次「创建房间」都会生成一个全新的房间。就算口令一样,也进不了以前的房间。
| 功能 | 说明 |
|---|---|
| 端到端加密 | 每对成员之间做 ECDH + ML-KEM-768 混合密钥交换(抗「先存后解」),每条消息都签名;中继看不到内容,也伪造不了消息 |
| 安全码 | 由全体成员的签名身份生成,用来发现密钥被调换 |
| 离线创建 | 空白文件和每个房间文件都能在断网时生成新房间,结果与官方构建逐字节一致 |
| 文件即服务器 | node index.html 直接运行中继,不用安装任何东西 |
| 房间标题 | 创建时可起名;持有文件的人以后可以改名,房间和口令不变 |
| 自定义中继 | 进房前可以改填任意 wss:// 中继(只对这次打开有效);也能生成换了中继的新文件发给大家 |
| 去中心化中继 | 可以用任意转发服务器、Nostr 中继、MQTT 代理,消息同时走多个中继 |
| 一个房间最多 50 人 | 写死在代码里,所有中继和页面执行同一个数 |
| 不留记录 | 不保存聊天记录;关掉页面,聊天内容就没了(对方仍可截图) |
聊天需要中继帮忙转发密文。中继有两类,不能混用:
| 类型 | 写法 | 特点 |
|---|---|---|
| 经典中继 | wss://relay.example.com(1–3 个) |
由中继分配成员号、限流和把门;坏了按顺序自动换下一个,连到不同中继的成员暂时看不到彼此 |
| 去中心化中继 | bc+wss://主机/bc/、nostr+wss://…、mqtt+wss://…/mqtt(1–5 个) |
中继只转发,成员自己协商编号和名单;消息同时走全部中继,只要有一个还通就能聊 |
- 在文件里临时换中继:新创建的房间文件默认允许自定义中继。进房前把「中继服务器」改成自己的地址即可,不改动文件。创建时取消勾选「允许在房间文件里自定义中继」,文件就只能连列出的中继,更严格。
- 让所有人一起换:打开房间文件,展开「生成新文件:换中继或改标题」,生成同一房间的新文件发给大家。旧文件也可以在创建页的「已经有房间文件?换一个中继服务器」里转换。
- 去中心化房间的代价:没有中继替你挡住乱发数据的人,他们解不开也伪造不了消息,但能浪费流量。公共 Nostr/MQTT 中继能看到 IP 和收发时间。去中心化中继只能广播,人多时流量增长很快,建议控制在 30 人左右;人多的房间用经典中继。
① 用房间文件本身(需要 Node.js 18+)
node index.html # 本机中继 + 创建页面 http://127.0.0.1:8080/
node index.html --tunnel # 再加一个免费公网地址(需要 cloudflared)
node index.html --help # 全部选项(端口、TLS 证书、允许的来源……)启动后会打印可以填进房间的中继地址。其他设备连接需要 wss://:加 --tunnel,或用 --tls-cert / --tls-key。
② 一条命令开单文件中继:不要域名、不要公网 IP、不要 root
curl -fsSL https://raw.githubusercontent.com/Freecode100Year/htmlroom/main/scripts/relay-quickstart.sh | sh -s -- --tunnel脚本下载对应平台的单文件中继(强制校验 SHA-256),并通过 Cloudflare 免费临时隧道得到公网地址,最后打印一行 bc+wss://xxxx.trycloudflare.com/bc/,填进房间即可。支持 Linux、macOS、FreeBSD、WSL、Termux 和 OpenWrt。Windows 请从 Releases 下载 htmlroom-relay-windows-amd64.exe,装好 cloudflared 后运行 htmlroom-relay-windows-amd64.exe -tunnel。临时隧道地址每次启动都会变;长期使用请配域名,见单文件中继。
③ 自己的服务器一键安装:Debian/Ubuntu,需要一个已解析到本机的域名,开放 80/443
curl -fsSL https://raw.githubusercontent.com/Freecode100Year/htmlroom/main/scripts/install.sh | sh -s -- --domain relay.example.com脚本检查 DNS、按需安装 Docker,用 Caddy 自动申请 HTTPS 证书,最后给出网页地址 https://relay.example.com/ 和中继地址 wss://relay.example.com。重复运行即更新,--uninstall 卸载(保留证书和你自己的文件)。详见服务器一键安装。
④ Cloudflare:网站和中继一起部署
网站和 Durable Objects 中继在同一个 Worker 上,部署后打开分配的 workers.dev 地址即可使用。仓库须公开。如果控制台不支持 JOIN_LIMIT 绑定,删掉 wrangler.jsonc 里的 ratelimits 后重新部署即可,人数和消息限制仍然生效。详见 Cloudflare 部署说明。
⑤ Vercel:只部署网站
部署时填写 HR_PAGES_RELAY(例如 wss://你的中继域名),作为创建页的默认中继。这一步不会部署中继:要从网站链接进经典中继的房间,还需在中继的 HR_ALLOWED_ORIGINS 里加入网站的 HTTPS 来源。从文件打开不受影响。详见 Vercel 部署说明。
| ✅ 可以 | ❌ 不可以 |
|---|---|
| 发文字消息,每条最多 500 字 | 发图片、文件、语音 |
| 一个房间最多 50 人 | 保存或找回聊天记录 |
| 断线后自动重连,消息有送达回执 | 不联网聊天(创建房间可以离线) |
| 每人约 6 秒发一条(防刷屏),多发的自动排队 | 防止对方截图或转述 |
- 中继能看到什么:看不到聊天内容;能看到谁在什么时间连进来、传了多少数据,也可能故意丢弃或延迟消息(回执会显示「未确认」)。
- 文件和链接就是钥匙:转给谁,谁就能进来。口令要足够长;像
1234这样的短口令,别人拿到文件后可以离线慢慢猜。 - 一定要核对安全码:这是发现冒充和密钥被调换的唯一办法。
- 核验文件:
node scripts/verify-room-file.mjs index.html会把整个文件(包括内嵌的服务器代码)与可复现构建逐字节比对。用node index.html运行文件,等于在电脑上执行其中的代码,请只运行你信任或核验过的文件。 - 局限:设备本身中了病毒,任何加密都保护不了你。ML-KEM 代码尚未经过独立审计,所以只与 ECDH 混合使用,不单独依赖。本项目也还没有经过第三方安全审计。
设了口令的房间文件,钥匙(32 字节主密钥)用口令派生的密钥包装起来。谁拿到文件,都可以在离线、不限次数地试口令——这是口令模式躲不开的威胁,所以口令的强度就是这道锁的全部强度。
这道锁有多慢。 口令不是直接当钥匙,而是先过一遍 PBKDF2-SHA-256、310000 轮,故意拖慢每一次尝试。实测本机单核浏览器加密每次约 48 毫秒(约 21 次/秒);下表按更强的攻击者估算,随机生成口令 指页面「随机生成口令」给出的 20 位口令(31 个不易混淆的字符,约 99 位熵)。时间按平均情况(试遍一半空间)计:
| 口令类型 | 熵 | 手机 / 笔记本 | 单张高端 GPU | 千卡集群 |
|---|---|---|---|---|
| 随机生成口令(20 位) | ~99 位 | 约 10²⁰ 年 | 约 10¹⁷ 年 | 约 10¹⁴ 年 |
| 8 位随机小写字母+数字 | ~41 位 | 约 450 年 | 约 330 天 | 约 8 小时 |
| 6 位随机小写字母 | ~28 位 | 约 18 天 | 约 51 分钟 | 约 3 秒 |
| 常见词 / 泄露库里的口令 | — | 约 2 个月 | 约 3 小时 | 约 10 秒 |
4 位数字 PIN(如 1234) |
~13 位 | 约 50 秒 | 瞬间 | 瞬间 |
攻击者算力按保守估计:手机/笔记本约 100 次/秒,单张高端 GPU 约 5 万次/秒,千卡集群约 5000 万次/秒。真实数字随硬件和优化浮动,但不同口令之间差了十几个数量级这一点不会变。
结论:
- 用「随机生成口令」。它比任何集群的寿命都长,这道锁实际上破不开。
- 自己想的口令:越长越乱越好,整句比单词强得多;别用生日、
1234、常见词或在别处用过的口令——这些在词典和泄露库里几秒钟就试到。 - 口令换一条独立渠道发(打电话、另一个聊天软件),别和文件一起发;310000 轮拖慢的是暴力破解,挡不住口令直接泄露。
- 想完全不靠口令强度,就别用口令模式,改成通过可信渠道把文件本身安全送达,谁都拿不到文件就无从离线破解。
这些数字只针对口令这把「外层锁」。聊天内容本身由每对成员的 ECDH + ML-KEM-768 会话密钥保护,和口令是两回事。表格里的估算脚本和 PBKDF2 参数可在安全模型查到。
日期:2026-10-29 执行模型:Claude Opus 4.8(claude-opus-4-8)
拿一个真实的加密房间文件(encrypted: true,默认中继 wss://chat.nya.je)做了两项测试。
1. 文件是否泄露口令 —— 没有。 文件里只有口令包装后的密文(salt 16 字节、iv 12 字节、ciphertext 48 字节)和 KDF 参数(PBKDF2-SHA-256,310000 轮),没有主密钥、也没有明文口令。想拿到口令,只能离线逐个猜。
2. 离线猜测有多快 —— 弱口令很快,生成口令不可行。 用项目自己的 unlock(),对一个临时创建、口令是随机 4 位 PIN 的房间做了完整爆破:第 483 次命中,单核约 20 次 / 秒(含 PBKDF2 310000 轮 + AES-GCM 解包)。按这个实测速度推算(与上表一致):
| 口令 | 本机单核实测速度下 |
|---|---|
| 4 位数字 PIN | 约 4 分钟(高端 GPU 上瞬间) |
| 常见词 / 泄露库里的口令 | 约 4 天(高端 GPU 上几十秒) |
| 「随机生成口令」(约 99 位熵) | 约 5×10²⁰ 年 |
结论: 房间文件本身守得住——它不交出口令。能不能被破,完全取决于口令强度:弱口令(PIN、常见词)在普通硬件上很快就试到,而页面「随机生成口令」给出的 20 位口令实际上破不开。请用「随机生成口令」,并通过独立渠道把口令告诉对方。
测试只针对口令这把外层锁,没有尝试破解任何真实房间,也没有在仓库里留下破解工具。方法:检查文件是否含明文密钥或主密钥,再用项目自带的
unlock()对一个自建的弱口令房间计时。
微信 / QQ / iPhone 里点开文件没反应?
这些 App 的「预览」经常跑不了聊天室,iPhone 的 Safari、Chrome 也不能直接打开手机里的 .html 文件。房间文件停在「正在启动」时,下方会出现「用浏览器打开这个房间」:点它(或复制链接粘贴到 Safari、Chrome)就能进房,口令不变。经典中继的房间打开中继自己的网页;Nostr、MQTT 等去中心化房间打开公共网站 https://hr50.freedom8964.com。链接里 # 后面的房间钥匙不会发给网站。旧文件没有这个链接,用新版的「换一个中继服务器」转换一次即可。在网站上创建房间时,「复制链接」旁边还有「生成二维码」,手机相机扫一下就能进房(二维码在页面里本地生成,不联网)。目前还没有在 iPhone、安卓、微信、QQ 上做过完整的实机测试。
node index.html 提示 Unknown file extension ".html"?
文件所在目录的上层有声明 "type": "module" 的 package.json。换个目录,或复制成 relay.cjs 再运行 node relay.cjs。
以前生成的房间文件还能用吗? 能用。新功能(离线创建、文件里自定义中继、标题)只在新版生成的文件里有。用新版的「换一个中继服务器」转换一次即可,房间和口令都不变。很早的单中继旧版(version 1)文件需要重新创建房间。
进房后一直显示「正在确认其他成员」? 大家的房间文件要一致,中继也要能连通;用的是经典中继的话,每个人要连同一个。换中继或有人进出后,等几秒让大家重新交换钥匙。
需要 Node.js 22 或更新:
git clone https://github.com/Freecode100Year/htmlroom.git
cd htmlroom
npm ci
npm start # http://localhost:8080/
npm test # 单元与中继测试(含 50 人满员测试)
npm run test:browser # 浏览器端到端测试(Playwright)主要目录:
html-room/:客户端与单文件模板。template.js生成整个文件,file.js是浏览器入口,serve.js是内嵌服务器,mesh.js负责去中心化中继。lib/:群组加密(group.js)。src/:Cloudflare Worker 与中继共用的规则;人数上限只在limits.js定义一次。server/:Node 中继。relay/go/:零依赖的单文件中继。scripts/:构建、核验、安装脚本。
文档:
本项目从 private-talk 中独立出来,详见来源说明。采用 MIT 许可证,保留原作者版权及第三方声明。