QR code 影格串流¶
什么时候用得上¶
由浅到深三种情况,你可能只符合第一种,那也够了。
一、手机里的东西要送进旁边那台笔记本,而现场的网络不是你的。咖啡厅、机场、别人公司的会议室,那些 Wi-Fi 你不知道谁在管。用通讯软件传给自己会在对方服务器留一份,U 盘跟数据线你不一定带着,而且手机接上别人的电脑时,跳出来问你要不要信任这台设备的那个对话框,本身就是一个要不要冒的险。这一页让你把手机屏幕举起来给笔记本的镜头看,东西就过去了。
二、要把东西交给眼前的人,而你们不想在任何地方留下往来记录。交换 PGP 公钥、给一行 Tor bridge 配置、把一份签好的文件交出去。用聊天软件传,两个账号就在对方的服务器上被记成一组关系。
三、其中一台机器刻意不联网,而你需要把东西拿进来或送出去。专门保管密钥或做签名的离线机器,插 U 盘等于破坏它不联网的前提。这一页只需要它有一个屏幕或一个镜头。要用在这种机器上,得在它进入断网状态之前先把这一页存成离线内容,之后就不必再碰网络。
三种情况共通的是:东西不大,两台设备就在旁边,而你不希望它经过任何网络。
如果你只是要把文件传给同事而且不介意经过网络,用你原本的方法就好,这一页对你没有好处,只会比较慢。
它是怎么做到的¶
把文件切成很多小段,每一段画成一张 QR code,然后在屏幕上一张一张翻,像一段会动的翻页动画。另一台设备拿摄像头一直对着看,把每一张收下来,全部收齐就拼回原文件。
两台设备从头到尾没有连在一起。没有配对蓝牙、没有连同一个 Wi-Fi、没有任何服务器经手,中间只有光。
三件常被问到的事先说明:
- 一个人也能用。两台设备都是你自己的完全没问题,手机传到笔记本是最常见的用法
- 一对多可以。十支手机同时对着同一个屏幕拍,每一支都会各自收齐,工作坊要发同一份配置给全场,播一次就好。实际上受限的是位置,每一支都要靠得够近、正面对着、让 QR code 占画面一半以上,围成一圈的话外圈那几支多半读不到。人多的时候用投影或大屏幕,并且把每张数据量往小调
- 方向可以换。哪一台发、哪一台收,每次自己决定,同一页两边都有
两个名词¶
后面的说明会一直用到,先定义清楚,不然数字会看不懂。
一「张」,指屏幕上的一个 QR code。一个文件会被切成很多张,一张一张轮流播。
一个「方格」,指一张 QR code 里面那些黑白小方块。一张 QR code 是由几千个方格排出来的。
先看它适合传什么¶
这个通道很慢,一秒钟大约送一到三 KB。慢是这个做法的代价,先看清楚再决定要不要用。
下面用「中」这一档为例。每张数据量有小、中、大、特大四档,默认是「中」,后面有完整的对照,这里先看它能传什么:
| 你要传的东西 | 大概多大 | 切成几张 | 播完一轮 |
|---|---|---|---|
| 一行 Tor bridge 配置 | 0.2 KB | 2 张 | 不到 1 秒 |
| 一份 PGP 公钥 | 10 KB | 27 张 | 5 秒 |
| 一份签好的文本文件 | 50 KB | 129 张 | 26 秒 |
| 一张手机拍的照片 | 3 MB | 超过上限 | 传不了 |
工具会挡下超过 512 KB 的文件。那个数字量的是人的耐性,不是技术极限。512 KB 用默认设置要切成 1302 张,播完一轮四分二十秒,而收齐通常不只一轮,等于要举着两台设备站十分钟以上。浏览器本身撑得住更多,撑不住的是人。调到最大的设置加最快的速度可以把一轮压到 42 秒,但那要条件很好才稳得住。
这个通道的天花板大约是每秒 12 KB。那是最大设置(每张 1264 个字节)配最快速度(每秒 10 张)算出来的,而且假设每一张都读到。一份 5 MB 的 PDF 照这个速度要七分钟,还是在完全不漏、不重播的前提下,实际上得举着两台设备站十几分钟。这种大小的文件超出了这个工具的范围,也不该硬做。
比这个大的东西请换别的方式。现场有网络的话用 OnionShare,没有网络的话用 U 盘。这一页适合的是几 KB 到几十 KB 的东西:密钥、配置文件、签名、短文本。
选好文件之后,工具会自己算给你看这一份切成几张、播一轮要多久。那个数字比上面的表准,因为它算的是你的文件跟你选的设置。
怎么用¶
需要两台设备,一台放文件,一台收文件。两台都打开这一页。
开始之前先想一下要传的东西该不该先加密
QR code 没有加密。播放中的画面被谁拍到,那个人就能把文件拼回去。判断的方法与加密的做法在传过去的东西是摊在屏幕上的那一节。
放文件的那一台,点上面的「发送」:
- 选文件,或把文件拖进那一块
- 等它算完,会告诉你切成几张、一轮多久
- 按「开始播放」,然后把屏幕举着不要动
收文件的那一台,点上面的「接收」,画面上也是三步:
- 打开相机。浏览器会问你要不要给摄像头权限,答应它
- 把镜头对准对方的画面。整张 QR code 都要在画面里,占画面一半以上最好读。收到的张数会实时往上跳,下面那排小方块会一块一块填实
- 收齐之后存下来。画面上会出现文件名、校验结果跟「保存文件」
还没轮到的步骤是淡的而且按不动,跟着亮起来的那一步走就好。
有两件事值得先知道。
放文件的那一台不知道对方收到多少,它只能照顺序一直重播。所以在对方说收齐之前不要停,也不要以为播完一轮就结束了。
播到一半屏幕自己熄掉是最常见的中断原因。开始之前先把屏幕的自动锁定时间调长一点。
不想给摄像头权限的话¶
接收那一端还有第二条路:按「改读视频或照片」。
拿手机原本的相机 App 把对方播放的画面录一段视频,录完把视频文件丢进这一页,工具会自己一张一张扫过去。这条路完全不会用到摄像头权限,在 Tor Browser 里也走得通。多张连拍的照片也可以。
录下来的视频要记得删掉
那段视频会存进你的相册,而相册多半开着云端自动备份。等于你为了不给摄像头权限,反而把整份文件的内容送上了别人的服务器。
要防的话,顺序很重要:录像之前先把相册的自动备份关掉。 事后删除挡不住已经同步出去的那一份,备份可能在你按下停止录像的几秒之内就完成,本机删掉不会让云端那一份消失。删除只防得住之后有人翻你的相册。
传完之后还是要把视频删掉并清空「最近删除」,那是第二道。真的在意这件事的话,用摄像头那条路反而干净,因为画面从来不会落成一个文件。
两个调整钮在调什么¶
「每张数据量」调的是一张装多少东西。装得多,一张里面的方格就多、每个方格就小,摄像头比较难对到,但总共不用播那么多张。装得少刚好相反,方格变大变好读,代价是张数变多、播一轮比较久。
| 每张数据量 | 一张装得下 | 一张有几个方格 | 容错度 | 中速 | 最快 |
|---|---|---|---|---|---|
| 小 | 204 个字节 | 57 × 57 | M | 每秒 1.0 KB | 每秒 2.0 KB |
| 中 | 403 个字节 | 77 × 77 | M | 每秒 2.0 KB | 每秒 3.9 KB |
| 大 | 849 个字节 | 97 × 97 | L | 每秒 4.1 KB | 每秒 8.3 KB |
| 特大 | 1264 个字节 | 117 × 117 | L | 每秒 6.2 KB | 每秒 12.3 KB |
用默认的「中」就好,这张表是给想知道为什么的人看的。容错度那一栏是 QR code 规格里的等级代号,不需要选,工具会照你挑的档位自己决定。
高的两档用比较低的容错度(L),所以同样的方格数装得下多两成八的数据。代价是能容忍的反光面积砍半,后面有测量的数字。
「特大」要条件够好才用得起来:发送端最好是屏幕比较大的设备,光线要够,镜头要靠近,而且不能有灯映在屏幕上。条件普通就用「中」,那一档在各种设备上都很稳,反光也扛得住。
「播放速度」调的是一张停留多久。太快的话,摄像头还没对好焦、还没曝光完,那一张就换掉了,拍到的是两张叠在一起的残影,一张都收不到。旧一点的手机或光线不好的场地就往慢的调。
对面读不到的时候¶
一张都收不到,或者收到一半就不动了,按这个顺序试:
- 把「每张数据量」往小调。这一项的效果最好。方格变大之后摄像头容易对到很多,代价只是多花一点时间
- 靠近一点,让 QR code 占满对方画面的一半以上
- 把「播放速度」往慢调,给摄像头多一点时间对焦
- 两边的屏幕亮度都调高,并且避开会在屏幕上反光的灯
- 正面对着拍。有点斜没关系,斜到快看不出方形就会失败
还有一件事帮得上忙:收的那一端会列出还缺哪几张,写成像 3-4、17 这样的编号。缺的如果是零星几张,多播一轮通常就补齐了。缺的如果是一整段,多半是那段时间镜头晃掉了或被东西挡住。
拿摄像头拍屏幕会出现一圈一圈的水波纹,叫摩尔纹。QR code 读取器那一页建议宁可截图也不要拍屏幕,就是因为它。这一页没得选,只能拍屏幕,所以方格大小跟距离比那一页更要紧。
为什么收到一半好像卡住了¶
放文件的那一台没有办法知道对方收到了什么,它只能照 0、1、2 的顺序一直重播。收的那一端漏掉的几张,要等下一轮转回来才补得到。
这在数学上叫 coupon collector,中文常译成集点问题:想集满一整套点数,越到后面越难抽到还缺的那几张。这里也一样,前面九成收得很快,最后几张要等好一阵子。
光线好、距离对的时候,一到两轮就满了。条件差的时候会拖到三四轮。看起来像卡住,画面上的进度还是在慢慢往上长,这时候不要停,调整一下距离跟角度就好。
传过去的东西是摊在屏幕上的¶
QR code 只是一种把数据画成图的方法,它没有加密。播放中的画面被谁拍到,那个人就能把你的文件拼回去,包括墙上的摄像头、对方手机的相册自动备份、还有站在你身后的人。
要传的东西本身敏感的话,先在自己的设备上把它加密好,再用这一页传加密后的文件,密码另外找别的管道给对方。加密的原理见端对端加密,实务上最常见的两种做法是用对方的 PGP 公钥加密,或是压成一个有密码的压缩包。
什么算敏感没有标准答案,可以用这一个问法自己判断:这份东西被现场的第三个人拍走,会有人受影响吗。公钥被拍走没事,那本来就是要公开的。Tor bridge 配置被拍走,可能让那个 bridge 被封锁,其他人就连不上,所以算。名单、草稿、还没公开的文件,都算。
工具会帮文件算一个校验码(SHA-256)放进第一张。校验码可以想成文件的指纹,发的一端先按一次指纹跟着送过去,收的一端拼完再按一次,两个一样就代表中间一个位都没有出错。收齐之后画面上会直接告诉你对不对得上。
要注意的是,指纹一样只证明文件没有在传输中坏掉,不证明文件是谁给的。对方是谁,还是要用别的方法确认,例如当面核对 PGP 指纹,或是回到已经建立信任的管道再确认一次。
跟 U 盘比呢¶
这是最常被拿来比的对象,因为 U 盘同样不经过网络、同样不留无线信号。诚实地说,两边各有输赢。
这一页赢在三件事。你不必交出一个实体物品,也就不必事后要回来。你不必把自己的 U 盘插进对方的机器,或是把对方的东西插进你的机器,那一插就是一次双向的风险。而且发出去的那一端传完之后,手上没有多一个装着那份文件的实体物品可以被搜到。
第三条只在发出去的那一端成立。收的一端不管用什么方式取得文件,东西都会落在自己的设备上,这一点见用完之后设备上留下什么。
U 盘赢在三件事。快得多,几 MB 的东西一秒钟就好。动作小,递过去一秒钟的事,不像举着两支手机对焦那样显眼。而且没有摄像头权限、没有浏览器、没有电量的问题。
两边一样的地方要说清楚:文件落到对方设备上之后,都是明文躺在那里。这一页保护的是传输过程不经过网络、不留下连接记录,它没有保护东西到了对方手上之后不被看到。如果你担心的是对方的设备事后被检查,那要先加密,跟用不用 U 盘无关。
用完之后设备上留下什么¶
这一节是给需要在事后被检查的场合用它的人看的。
工具本身不写任何本机存储。没有用 localStorage、IndexedDB、cookie,也没有写进缓存。你选的文件跟收到的内容都只活在标签页的内存里,关掉标签页就没了。
存下来的文件是一般文件。按了「保存文件」之后,它就跟任何下载一样躺在下载文件夹,也会出现在浏览器的下载记录里,要清就照平常清下载记录的方式清。
用摄像头录的视频留在相册里。见上面「不想给摄像头权限的话」那一段。这是整页最容易被忽略的残留。
每次发送的识别码不会变成线索。每一张 QR code 里有一个 sessionId,用途只是让收的一端认出对方换了文件。它每一份文件重新抽一次随机数,不跟设备绑定、不会存起来,所以同一台设备接两份不同的文件,两次的识别码对不起来,没办法拿来推论是同一个人操作。
这一页本身要先加载过。第一次打开这一页需要网络,因为程序要从站上下载。真正要在断网的场合用它,请事先在有网络的时候打开这一页并且存成离线内容,之后没有网络也打得开。
为什么没有 Wi-Fi 或蓝牙版本¶
这是最常被问的一题。简短的答案是浏览器没有那些能力,跟想不想做无关。
蓝牙那边¶
浏览器碰得到的蓝牙功能只设计来跟手环、体重计那类小设备交换很短的数值,规格里根本没有传文件这件事。而且网页只能扮演「主动去连别人」的那一方,没办法让别人来连自己,所以两个网页永远看不见对方。再加上 Firefox 完全没有实现这个功能,以它为基础的 Tor Browser 也就没有。
Wi-Fi 那边¶
网页根本碰不到 Wi-Fi。它不能扫描附近有哪些网络,也不能开一个通道让对方连进来。同一个局域网里唯一的点对点通道是视频通话用的那套技术(WebRTC),而 Tor Browser 把它整个关掉,因为它会泄漏你真正的 IP。公共 Wi-Fi 也普遍禁止同一个网络里的设备互相连接,而那正好是最需要用到这种功能的场合。
扣掉那些,剩下唯一走得通的通道就是屏幕跟镜头。慢,但是在浏览器里做得到,在 Tor Browser 里也能用。
不开任何无线功能还有一个顺带的好处:两台设备之间没有任何连接可以被侦测到,也不会留下无线信号。在会被观察的场合交东西,这一点比速度重要。
给要自己实现的人¶
以下是给想自己写另一端的人看的技术规格。一般使用完全不需要读,可以直接跳到用了谁的程序。
用屏幕跟镜头跨过一段刻意不连接的距离,这个做法叫 air-gapped 传输。硬件钱包把交易数据拿进不联网的机器签名、再把签好的结果拿出来,用的就是同一套办法,动作跟这一页一模一样。
每一张是原始字节,走 QR code 的 byte mode。QR code 有好几种装数据的方式,byte mode 是可以装任意字节的那一种,所以不限文字,压缩包跟签名文件都传得过去。
| 位置 | 长度 | 内容 |
|---|---|---|
| 0 | 1 | 0xA1,标记与版本 |
| 1-2 | 2 | sessionId,用来认出对面换了文件 |
| 3-4 | 2 | 总张数,含编号 0 那一张 |
| 5-6 | 2 | 这一张的编号 |
| 7 起 | 不定 | 内容 |
| 最后 2 个 | 2 | CRC-16/CCITT-FALSE,盖住它前面的每一个字节 |
sessionId 或总张数只要对不上,收的一端就把手上收到的整批丢掉重新开始,并且在画面上说一声。这个工具两端都会做这件事,使用者不需要担心。
写另一端的人要注意的是漏掉这个检查的后果:两份文件的张数混在一起会拼出一个谁都没发过的文件,而发出去的那一端如果没附校验码,收的人不会发现。
编号 0 那一张放的是一段 JSON:n 文件名、s 原始大小、c 实际发出的字节数(没压缩时省略)、h 原始文件的 SHA-256、z 有没有用 deflate-raw 压缩过。编号 1 之后照编号接起来就是文件。
为什么一张只装得下这么少¶
QR code 规格的上限是版本 40 配 L 级,2953 个字节。这个工具最大的一档只有 988,差了三倍。两个原因,都量过。
版本停在 25,因为再上去解不出来¶
决定成败的是「一个方格在对方摄像头拍到的画面里占几个像素」。固定取景(QR 横跨画面里 768 个像素,大约是占满六成宽)扫过各版本,崩溃点在每个方格 4.5 到 5 个像素之间:
| 版本 | 方格 | 每格占几个摄像头像素 | 解得出来的比例 |
|---|---|---|---|
| 25 | 117 × 117 | 6.1 | 100% |
| 30 | 137 × 137 | 5.3 | 90% |
| 35 | 157 × 157 | 4.7 | 70% |
| 40 | 177 × 177 | 4.2 | 0% |
版本 30 看起来只掉一成,实际代价大得多。发的一端没有反馈通道,每张的成功率掉到九成,收齐需要的轮数大约从一轮变成两轮多,而每张多装的三成八根本补不回来。算下来版本 30 传同一个文件反而比版本 25 慢五成以上。这就是前面「集点问题」那一节的实际后果。
容错度低两档用 M,高两档用 L¶
同样的测量显示,分辨率不够的时候 L 跟 M 的解码率一模一样,失败是整片认不出来,容错度救不了。容错度真正在保护的是局部破坏,也就是天花板的灯映在屏幕上那一块全白的斑:
| 容错度 | 版本 25 一张装得下 | 光斑盖掉 5% | 盖掉 10% | 盖掉 15% |
|---|---|---|---|---|
| L | 1264 个字节 | 读得到 | 读不到 | 读不到 |
| M | 988 个字节 | 读得到 | 读得到 | 读不到 |
| Q | 706 个字节 | 读得到 | 读得到 | 大致读得到 |
同一个版本从 M 换成 L,方格数一个都没变,数据多两成八。以分辨率来说完全免费,付的是反光的余裕。
所以四档分成两段。高两档用 L:按到那里的人是在要速度,而且那两档本来就要条件够好才用得起来。低两档留 M:那两档是「怎么样都读不到」的时候退回来的地方,退路本身不该是脆弱的,而室内反光盖掉一成码面很常见,正好是 L 死掉而 M 还活着的区间。
测量用的脚本在 tools/measure_qrstream_density.mjs,可以自己重新执行。它模拟的是失焦、传感器噪声与反光,没有模拟摩尔纹、透视变形与手抖,所以算出来的数字偏乐观,实际条件只会更差。这也是默认值留余裕的理由。
每一张最后还是加了一个 CRC¶
CRC 是一个很短的检查码,用来确认这一张从头到尾没有被读错。QR code 自己的修复能力已经让读错的概率很低,这一层看起来多余,但它换掉的失败模式很值得:少了它,一张读错要等到全部收完、比对 SHA-256 对不上才发现,那时候已经不知道是哪一张坏掉,只能整份重来。有了它,坏掉的那一张当场丢掉,下一轮再收一次就好。两个字节买到这个,很划算。
没有做喷泉码¶
前面提到的集点问题有一个对症的解法,叫喷泉码(fountain code),它的性质是只要收到足够多张就能还原,跟收到的是哪几张无关。目前没有做,因为那套格式没办法用手验算,而这一页的重点之一是任何人都能自己检查它做了什么。
用了谁的程序¶
编码交给 qrcode-generator(MIT 许可),解码交给 jsQR(Apache-2.0 许可,许可全文),两份都原封不动放在 utils/vendor/ 底下,跟生成器与读取器共用同一份。这一区用到的第三方组件都列在小工具首页。
切张、拼回与那些检查是自己写的,测试在 tools/test_qrstream.mjs。那支拿真的两个函式库走完整条路(编码、放大成像素、解码),并且刻意漏张、乱序、改掉字节、中途换文件,确认每一种都得到该有的结果。
界面另外有两支。tools/check_qrstream_ui.mjs 用 DOM 替身把整支执行一遍,验接线。tools/check_qrstream_browser.mjs 开一颗真的浏览器,量每一段文字的对比有没有过 WCAG AA,并且拿一个 Y4M 文件当假的摄像头,让收文件那条路从 getUserMedia 一路走到存档。摄像头那一段没有真浏览器就测不到,而它正是这一页的主要用法。
离线可用¶
跟这一区其他工具一样,程序存进设备之后没有网络也能用。要离线带着走,见离线阅读。
这一页的断网可用有一个额外的意义:两台设备都关掉网络照样传得成,那本身就是「文件没有经过任何服务器」的证明。想确认的话,把两边都切成飞行模式再传一次。