跳轉到

QR code 影格串流

什麼時候用得上

由淺到深三種情況,你可能只符合第一種,那也夠了。

一、手機裡的東西要送進旁邊那台筆電,而現場的網路不是你的。咖啡廳、機場、別人公司的會議室,那些 Wi-Fi 你不知道誰在管。用通訊軟體傳給自己會在對方伺服器留一份,隨身碟跟傳輸線你不一定帶著,而且手機接上別人的電腦時,跳出來問你要不要信任這台裝置的那個對話框,本身就是一個要不要冒的險。這一頁讓你把手機螢幕舉起來給筆電的鏡頭看,東西就過去了。

二、要把東西交給眼前的人,而你們不想在任何地方留下往來紀錄。交換 PGP 公鑰、給一行 Tor bridge 設定、把一份簽好的檔案交出去。用聊天軟體傳,兩個帳號就在對方的伺服器上被記成一組關係。

三、其中一台機器刻意不連網,而你需要把東西拿進來或送出去。專門保管金鑰或做簽章的離線機器,插隨身碟等於破壞它不連網的前提。這一頁只需要它有一個螢幕或一個鏡頭。要用在這種機器上,得在它進入斷網狀態之前先把這一頁存成離線內容,之後就不必再碰網路。

三種情況共通的是:東西不大,兩台裝置就在旁邊,而你不希望它經過任何網路。

如果你只是要把檔案傳給同事而且不介意經過網路,用你原本的方法就好,這一頁對你沒有好處,只會比較慢。

它是怎麼做到的

把檔案切成很多小段,每一段畫成一張 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,沒有網路的話用隨身碟。這一頁適合的是幾 KB 到幾十 KB 的東西:金鑰、設定檔、簽章、短文字。

選好檔案之後,工具會自己算給你看這一份切成幾張、播一輪要多久。那個數字比上面的表準,因為它算的是你的檔案跟你選的設定。

怎麼用

需要兩台裝置,一台放檔案,一台收檔案。兩台都打開這一頁。

開始之前先想一下要傳的東西該不該先加密

QR code 沒有加密。播放中的畫面被誰拍到,那個人就能把檔案拼回去。判斷的方法與加密的做法在傳過去的東西是攤在螢幕上的那一節。

放檔案的那一台,點上面的「傳送」:

  1. 選檔案,或把檔案拖進那一塊
  2. 等它算完,會告訴你切成幾張、一輪多久
  3. 按「開始播放」,然後把螢幕舉著不要動

收檔案的那一台,點上面的「接收」,畫面上也是三步:

  1. 打開相機。瀏覽器會問你要不要給相機權限,答應它
  2. 把鏡頭對準對方的畫面。整張 QR code 都要在畫面裡,佔畫面一半以上最好讀。收到的張數會即時往上跳,下面那排小方塊會一塊一塊填實
  3. 收齊之後存下來。畫面上會出現檔名、校驗結果跟「儲存檔案」

還沒輪到的步驟是淡的而且按不動,跟著亮起來的那一步走就好。

有兩件事值得先知道。

放檔案的那一台不知道對方收到多少,它只能照順序一直重播。所以在對方說收齊之前不要停,也不要以為播完一輪就結束了。

播到一半螢幕自己熄掉是最常見的中斷原因。開始之前先把螢幕的自動鎖定時間調長一點。

不想給相機權限的話

接收那一端還有第二條路:按「改讀影片或照片」。

拿手機原本的相機 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),所以同樣的方格數裝得下多兩成八的資料。代價是能容忍的反光面積砍半,後面有量測的數字

「特大」要條件夠好才用得起來:發送端最好是螢幕比較大的裝置,光線要夠,鏡頭要靠近,而且不能有燈映在螢幕上。條件普通就用「中」,那一檔在各種裝置上都很穩,反光也扛得住。

「播放速度」調的是一張停留多久。太快的話,相機還沒對好焦、還沒曝光完,那一張就換掉了,拍到的是兩張疊在一起的殘影,一張都收不到。舊一點的手機或光線不好的場地就往慢的調。

對面讀不到的時候

一張都收不到,或者收到一半就不動了,按這個順序試:

  1. 把「每張資料量」往小調。這一項的效果最好。方格變大之後相機容易對到很多,代價只是多花一點時間
  2. 靠近一點,讓 QR code 佔滿對方畫面的一半以上
  3. 把「播放速度」往慢調,給相機多一點時間對焦
  4. 兩邊的螢幕亮度都調高,並且避開會在螢幕上反光的燈
  5. 正面對著拍。有點斜沒關係,斜到快看不出方形就會失敗

還有一件事幫得上忙:收的那一端會列出還缺哪幾張,寫成像 3-417 這樣的編號。缺的如果是零星幾張,多播一輪通常就補齊了。缺的如果是一整段,多半是那段時間鏡頭晃掉了或被東西擋住。

拿相機拍螢幕會出現一圈一圈的水波紋,叫摩爾紋。QR code 讀取器那一頁建議寧可截圖也不要拍螢幕,就是因為它。這一頁沒得選,只能拍螢幕,所以方格大小跟距離比那一頁更要緊。

為什麼收到一半好像卡住了

放檔案的那一台沒有辦法知道對方收到了什麼,它只能照 012 的順序一直重播。收的那一端漏掉的幾張,要等下一輪轉回來才補得到。

這在數學上叫 coupon collector,中文常譯成集點問題:想集滿一整套點數,越到後面越難抽到還缺的那幾張。這裡也一樣,前面九成收得很快,最後幾張要等好一陣子。

光線好、距離對的時候,一到兩輪就滿了。條件差的時候會拖到三四輪。看起來像卡住,畫面上的進度還是在慢慢往上長,這時候不要停,調整一下距離跟角度就好。

傳過去的東西是攤在螢幕上的

QR code 只是一種把資料畫成圖的方法,它沒有加密。播放中的畫面被誰拍到,那個人就能把你的檔案拼回去,包括牆上的攝影機、對方手機的相簿自動備份、還有站在你身後的人。

要傳的東西本身敏感的話,先在自己的裝置上把它加密好,再用這一頁傳加密後的檔案,密碼另外找別的管道給對方。加密的原理見端對端加密,實務上最常見的兩種做法是用對方的 PGP 公鑰加密,或是壓成一個有密碼的壓縮檔。

什麼算敏感沒有標準答案,可以用這一個問法自己判斷:這份東西被現場的第三個人拍走,會有人受影響嗎。公鑰被拍走沒事,那本來就是要公開的。Tor bridge 設定被拍走,可能讓那個 bridge 被封鎖,其他人就連不上,所以算。名單、草稿、還沒公開的文件,都算。

工具會幫檔案算一個校驗碼(SHA-256)放進第一張。校驗碼可以想成檔案的指紋,送的一端先按一次指紋跟著送過去,收的一端拼完再按一次,兩個一樣就代表中間一個位元都沒有出錯。收齊之後畫面上會直接告訴你對不對得上。

要注意的是,指紋一樣只證明檔案沒有在傳輸中壞掉,不證明檔案是誰給的。對方是誰,還是要用別的方法確認,例如當面核對 PGP 指紋,或是回到已經建立信任的管道再確認一次。

跟隨身碟比呢

這是最常被拿來比的對象,因為隨身碟同樣不經過網路、同樣不留無線訊號。誠實地說,兩邊各有輸贏。

這一頁贏在三件事。你不必交出一個實體物品,也就不必事後要回來。你不必把自己的隨身碟插進對方的機器,或是把對方的東西插進你的機器,那一插就是一次雙向的風險。而且送出去的那一端傳完之後,手上沒有多一個裝著那份檔案的實體物品可以被搜到。

第三條只在送出去的那一端成立。收的一端不管用什麼方式取得檔案,東西都會落在自己的裝置上,這一點見用完之後裝置上留下什麼

隨身碟贏在三件事。快得多,幾 MB 的東西一秒鐘就好。動作小,遞過去一秒鐘的事,不像舉著兩支手機對焦那樣顯眼。而且沒有相機權限、沒有瀏覽器、沒有電量的問題。

兩邊一樣的地方要說清楚:檔案落到對方裝置上之後,都是明文躺在那裡。這一頁保護的是傳輸過程不經過網路、不留下連線紀錄,它沒有保護東西到了對方手上之後不被看到。如果你擔心的是對方的裝置事後被檢查,那要先加密,跟用不用隨身碟無關。

用完之後裝置上留下什麼

這一節是給需要在事後被檢查的場合用它的人看的。

工具本身不寫任何本機儲存。沒有用 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 一路走到存檔。相機那一段沒有真瀏覽器就測不到,而它正是這一頁的主要用法。

離線可用

跟這一區其他工具一樣,程式存進裝置之後沒有網路也能用。要離線帶著走,見離線閱讀

這一頁的斷網可用有一個額外的意義:兩台裝置都關掉網路照樣傳得成,那本身就是「檔案沒有經過任何伺服器」的證明。想確認的話,把兩邊都切成飛航模式再傳一次。