01
使用机器人不需要下面这些
使用机器人就是照常使用应用。如果有人给你一个机器人的邀请(文本或二维码,通常公布在它的网站上):
- 如果是频道:新建 → 加入频道,粘贴邀请文本(以
UEND开头)或扫描其二维码,然后加入。进入后,刷新会拉取内容;第一次拉取可能要等一两分钟,直到频道地址在 Tor 上发布。 - 如果是联系人:新建 → 新联系人,粘贴邀请或扫描机器人的动画二维码,起个名字,添加。你的第一条消息带着加密握手;到达后,机器人把你当作联系人,并回复它的程序所规定的内容。
- 验证它:在聊天里,更多 → 当面验证 → 粘贴对方的代码,用运营者在其网站上签名公布的验证码核对。
而下面这些是机器人做不到的——由协议设计保证:
- 读不到你没有亲自发给它的任何东西。你的对话、群组和频道,它既看不到,也不知道它们存在。
- 进不了没有邀请它的地方。它只有在管理员像介绍任何成员那样介绍它时才能进群;也不能向别人的频道发布:每个频道只有持有其密钥的人才能写。
- 不知道谁在读它的频道。读者通过匿名链路拉取内容,从不表明身份。
- 不存在 PrivChat「官方」机器人。每个机器人由它的运营者负责——你写给机器人的内容就是交给了运营者。如果背后是谁对你重要,请用它公布的验证码核对。
02
这里的机器人是什么
- 只是另一个节点。它有自己的身份、自己的 Tor onion 服务、自己的加密数据库和自己的频道。它就是应用的引擎,只是不带 Android 编译而成。
- 在线路上无法分辨。同样的信封、同样的加密(Double Ratchet 与 PQXDH)、同样的掩护流量、同样的频道。应用不知道自己在和一个程序对话,因为根本没有办法知道。
- 没有目录。一个中央目录就是这个应用存在的意义所要避免的那台服务器。运营者把机器人的邀请发到任何他想发的地方:网页、海报、一条消息。
- 本地 API。你的程序(Python、Go、shell,什么都行)通过机器人目录下的 Unix 套接字、以 JSON 与它对话。远程的部分由 Tor 负责。
- 两种用途,按这个顺序。频道:机器人发布,任何人匿名阅读。以及一对一:人们给它写信,机器人回答。
03
机器人能看到什么,看不到什么
关于它频道的读者,什么都看不到。读者通过匿名链路拉取内容,不表明身份;机器人只看到有人读过、何时、多少,再多也看不到,因为协议里没有地方放这些信息。有一千个读者的频道对读者的了解和只有十个读者的频道完全一样。
关于给它写信的人,看到的和任何联系人一样。给机器人写信,就是把你的标识、你的 Tor 回信地址和你写的内容交给它的运营者。对一个人类联系人来说这些是分散的;对一个热门机器人来说,这些集中在一台服务器上。所以频道排在前面,所以机器人不保存历史:一条收到的消息只存活到运营者的程序确认它为止,上限七天。
如果机器人的服务器落入坏人之手,他们会拿走机器人的身份(从而能永远冒充它:没有撤销机制)、它频道的密钥和它的联系人列表。他们拿不到任何用户或任何读者的任何东西。恢复方法是销毁这个机器人、创建一个新的,并在其他渠道公布新的指纹。
没有任何东西会告诉你某个联系人是机器人,也不会告诉你它就是它自称的那个机器人。认真的运营者会在自己的网站上公布经签名的机器人验证码;在应用的聊天里,更多 → 当面验证 → 粘贴对方的代码(或扫描它的二维码),就像验证一个人那样核对。
04
部署一个机器人
这部分是运营者的事。需要一台自己的 Linux——服务器,或 64 位的树莓派 4 或 5——、Java 17 或更新版本(sudo apt install openjdk-17-jre-headless)、大约 300 MB 空闲内存,以及通往 Tor 的出口。手机当不了机器人:机器人必须一直开机、一直在线,那得是运营者的电脑,不是手机。
安装包:privchat-bot-2.19.2.zip(脚本、库和原生 arti;Linux x86_64——Windows 仅用于开发)。树莓派(aarch64)的安装包会在真机测试通过后发布,绝不提前。
安装包的 SHA-256:
18d98f258b9206dd2be9f8e89bd19b18db5fb55eee53ee8fc6e7745573f8f6fe
sudo unzip privchat-bot-2.19.2.zip -d /opt && sudo mv /opt/privchat-bot* /opt/privchat-bot
sudo useradd --system --home /var/lib/privchat-bot --shell /usr/sbin/nologin privchat-bot
sudo install -d -o privchat-bot -g privchat-bot -m 0700 /var/lib/privchat-bot
# 创建机器人:封印、身份、prekeys、数据库。密码短语只显示一次。
sudo -u privchat-bot /opt/privchat-bot/bin/privchat-bot --datos /var/lib/privchat-bot init --passphrase-generada
# 密码短语放进只有 root 能读的凭据文件(永远不要放进环境变量)
sudo install -d -m 0700 /etc/privchat-bot
sudo install -m 0600 /dev/stdin /etc/privchat-bot/passphrase <<< 'init-显示的密码短语'
# 服务
sudo cp /opt/privchat-bot/etc/privchat-bot.service /etc/systemd/system/
sudo systemctl enable --now privchat-bot
密码短语是对付复制磁盘者的全部防线。服务器没有手机的 Keystore,假装有会比说实话更糟。要长,或者由程序生成;有 TPM 2.0 时,用 systemd-creds encrypt --with-key=tpm2 和单元文件里的 LoadCredentialEncrypted= 把它绑定到机器;没有 TPM 时,把 0600 的文件放在加密磁盘上。机器人没有 PIN,也没有锁屏:没有人在它面前打字。
05
参数
| 选项 | 作用 | 默认值 |
|---|---|---|
--datos DIR | 机器人的目录 | ./privchat-bot |
--credencial FILE | 存放在 0600 文件中的密码短语 | systemd 的凭据,或控制台 |
--socket PATH | API 的 Unix 套接字(路径要短:AF_UNIX 只允许约 100 字节) | DIR/api.sock |
--api-tcp PORT | 另外在 127.0.0.1 上带令牌开放 API | 关闭 |
--api-grupo GROUP | 该系统组也可打开套接字(0660) | 仅用户本人(0600) |
--retencion-dias N | 未确认的收到消息存活的天数 | 7(最多 30) |
--max-contactos N | 通过公开邀请加入的联系人上限 | 1000 |
--sin-relleno | 关闭掩护流量 | 开启,与应用相同 |
--buzon | 作为主人的信箱:除 Tor 外,还使用“面对面”的本地网络 | 关闭 |
--bluetooth | 与 --buzon 一起使用时,也通过低功耗蓝牙(仅 Linux,BlueZ) | 关闭 |
--malla 端口 | LoRa 网状网络:通过 USB(/dev/ttyACM0、COM4)或 TCP(host:4403)连接 Meshtastic 节点;Tor 无法送达时文字通过无线电发出(机器人 2.16.1) | 关闭 |
--verboso | 详细日志,包括 Tor 的日志。仅用于调试 | 关闭 |
不需要守护进程的命令:init、passphrase cambiar、copia-seguridad DEST.zip(用于在另一台机器上恢复,绝不用于同时运行两个相同的机器人)、restaurar SOURCE.zip 和 destruir --si。日志里永远没有 onion 地址或完整标识,而且任何一次被响应的频道读取都不会留下记录。
06
从机器人一侧操作频道
B="sudo -u privchat-bot /opt/privchat-bot/bin/privchat-bot --datos /var/lib/privchat-bot"
$B canal crear "小镇通知" "停水与节庆" # 返回 canal_id
$B canal invitacion <canal_id> --png /var/www/avisos.png # 可粘贴的文本,以及 PNG 里的二维码
$B canal publicar <canal_id> "明天 9 点到 12 点停水"
$B canal publicar-foto <canal_id> /path/haibao.jpg "海报" # 会被重新压缩:EXIF 在这里消失
$B canal listar # 内容数、拉取次数和已服务字节数
$B canal recortar <canal_id> 50 # 只保留最近 50 条
每条内容都由频道密钥签名、用其读取密钥加密并与前一条相连:历史可以停止提供,但不能在读者毫无察觉的情况下被改写。每条文本最多 12 KB;照片会被重新压缩直到装进 900 KB。
07
一对一联系人
两种邀请,格式与应用里的相同:
| 个人邀请 | 公开邀请(--publica) | |
|---|---|---|
| 谁能使用 | 一个人:第一个;第二个会被拒绝 | 任何人,直到 --max-contactos 上限 |
| 一次性 prekeys | 五个,与应用相同 | 没有:分发给成千上万的人,它们就不再是一次性的了 |
| 代价 | 没有新的代价 | 每个用户发给机器人的第一条消息,在机器人密钥未来被攻破的情况下,缺少 prekey 提供的额外保护;机器人一回复,ratchet 就会自愈 |
| 用途 | 给自己的助手、给家人的通知 | 面向公众开放的机器人 |
$B invitacion crear web --publica # 可粘贴的文本
$B invitacion crear web --publica --gif /var/www/bot.gif # 外加邀请的四个二维码,做成动画 GIF
$B verificacion codigo --png /var/www/verificacion.png # 签名后公布:人们由此知道这个机器人就是那个机器人
$B mensaje escuchar --reponer # JSON 事件,每行一个
$B mensaje enviar <peer_id> "你好"
$B mensaje enviar <peer_id> "你好" --retardo 3000 # 在接下来 3 秒内的随机时刻发出
$B mensaje confirmar <msg_id> # 机器人忘掉它
08
把机器人当作家中的信箱
加上 --buzon,机器人就成为某个人(它的主人)的信箱:放在家中的树莓派或电脑上,在主人不在时保管并转交其消息。来到该 Wi‑Fi 的人会留下给主人的消息,并取走主人留给他们的消息;如果主人在手机上开启“联网”,其联系人也能在手机关机时通过 Tor 留言。在 systemd 单元中,把 --buzon 加到 ExecStart 里,放在 run 之前。
$B buzon invitar # 在手机上粘贴这段文字:设置 → 我的信箱;第一个使用它的人就是主人
$B buzon estado # 主人以及保管的内容
蓝牙(仅 Linux,可选)。使用 --buzon --bluetooth 时,信箱也可以通过低功耗蓝牙被找到:为主人开启了“面对面聊天”的人经过门口时,无需连接 Wi‑Fi 就能留下和取走消息。需要运行中的 BlueZ、一个适配器、服务用户属于 bluetooth 组,并在 /etc/bluetooth/main.conf 的 [General] 部分设置 ReverseServiceDiscovery = false(然后重启 bluetooth 服务):否则手机会弹出配对请求,通道无法建立。该设置也会关闭 BlueZ 对其他低功耗蓝牙设备的 GATT 客户端,所以最好用在专用设备上。默认关闭,因为广播会让在街上扫描的人知道这里有 PrivChat,尽管不知道是谁的。
它只保管主人的消息(密封的),以及主人允许的联系人的每日密钥:从不保管主人的身份。如果被偷,它什么也读不到,大约 48 小时后就不再认得任何人;可以在手机上解除配对。它的 .onion 地址从第一次启动起就是固定的,即使从未联网。在手机上的样子见使用指南。
09
为机器人编程:本地 API
每行一个请求或响应,JSON,通过套接字传输。认证就是文件系统:能打开套接字的人就是能读机器人目录的人。suscribir 之后,事件通过同一连接到达。
→ {"id": 1, "op": "estado"}
← {"id": 1, "ok": true, "peer_id": "…", "huella": "…", "onion": "….onion", "tor": "online", "contactos": 3, "canales": 1}
→ {"id": 2, "op": "canal.publicar", "canal_id": "…", "texto": "早上好"}
← {"id": 2, "ok": true, "seq": 42}
← {"evento": "mensaje.recibido", "peer_id": "…", "msg_id": "…", "texto": "你好", "fecha": 1788500000}
| 操作 | 参数 | 返回 |
|---|---|---|
estado | — | 身份、onion、Tor 状态、计数器 |
suscribir | reponer | 包含未确认消息的 eventos[];之后是实时事件 |
invitacion.crear | etiqueta、publica、qr | invite_id、texto、qr_gif |
contacto.listar / renombrar / borrar | peer_id… | — |
verificacion.codigo / comprobar | peer_id、codigo | texto 与二维码 / resultado |
mensaje.enviar | peer_id、texto、respuesta_a、retardo_ms | msg_id |
adjunto.enviar / adjunto.leer | peer_id、nombre、base64 或 ruta / msg_id | msg_id / 字节 |
mensaje.estado / mensaje.confirmar | msg_id | 状态 / —(删除) |
canal.crear / invitacion / publicar / publicar_foto / recortar / borrar | canal_id、texto、pie… | seq、texto 与二维码… |
事件:mensaje.recibido、adjunto.recibido、contacto.nuevo、entregado、tor。它们至少送达一次:一条消息会在每次 suscribir 时重放,直到你确认它;msg_id 让你去重。错误带有稳定的代码(sin_sesion、invitacion_invalida、demasiado_grande…);内部错误永远不会把细节传给客户端。操作名和字段名是西班牙语,即项目的语言:它们是稳定的契约,不是散文。
一个完整的回声机器人,用 Python,无依赖:
import json, socket
s = socket.socket(socket.AF_UNIX); s.connect("/var/lib/privchat-bot/api.sock")
f = s.makefile("rw", encoding="utf-8")
def call(**p):
f.write(json.dumps(p) + "\n"); f.flush(); return json.loads(f.readline())
r = call(id=1, op="suscribir", reponer=True)
for ev in list(r["eventos"]) + [json.loads(l) for l in f]:
if ev.get("evento") == "mensaje.recibido":
call(id=2, op="mensaje.enviar", peer_id=ev["peer_id"], texto="回声:" + ev["texto"], respuesta_a=ev["msg_id"])
call(id=3, op="mensaje.confirmar", msg_id=ev["msg_id"])
包里附带两个完整示例:eco.py(把响应与交错到达的事件分开,机器人一边监听一边回复时必须这样做)和 rss_a_canal.py,它把 RSS 或 Atom 订阅源的每条新条目发布到频道。
10
机器人必须遵守的规则
- 确认你处理过的内容;未确认的会重放给你,七天后删除。
- 不要下载别人发给你的链接:一个能被驱使去请求任意 URL 的机器人,总会有人拿来干这个。
- 把文本当作数据:它来自陌生人。
- 上限:频道文本每条 12 KB,附件每个 7.5 MB,频道照片每张 900 KB,API 每行 16 MB。
- 规模:几百个一对一联系人可以;上千,请用频道。
- 一个永不下线的 onion 比手机更容易被追溯到它的守卫节点;所以机器人启用了 vanguards,也所以它不该和你不想与之关联的东西跑在一起。
11
常见问题
| 症状 | 可能原因 | 怎么办 |
|---|---|---|
tor: arrancando 持续数分钟 | 没有网络,或者杀毒软件、代理在拦截 TLS | 关闭拦截;--verboso 会显示 Tor 给出的原因 |
| «Unix domain path too long» | 套接字路径超过约 100 字节 | --socket /tmp/pb.sock |
| 应用的第一次拉取什么都没拿到 | 频道地址尚未在 Tor 上发布 | 等一两分钟再刷新 |
| 一条消息在「队列中」停留一段时间 | rendezvous 链路建立需要时间 | 等待;消息一到,机器人就会确认 |
contacto listar 没有显示刚添加机器人的人 | 机器人是通过对方的第一条消息才知道这个联系人的 | 让对方写点什么 |
发送时出现 sin_sesion | 那个 peer_id 还不是联系人 | contacto listar |