我做了一個隱形浮水印實驗室,然後被它抓到三次講錯話

產一把金鑰,蓋在圖上,肉眼看不出差別。再用別人的金鑰試著偽造、把圖摧殘到它死掉為止。每一步的中間產物都畫出來。過程中我原本寫在文件裡的三句話,被實測一句一句推翻。

隱形浮水印實驗室

隱形浮水印實驗室 做的事很單純:產一把金鑰,蓋在你的作品上,肉眼看不出任何差別。然後驗給自己看、試著用別人的金鑰偽造、把圖裁一半縮一半再壓一輪,看它什麼時候會死。

每一步的中間產物都畫在頁面上,沒有黑箱。所有運算都在你自己的瀏覽器裡跑,圖片跟金鑰不會離開你的電腦。

這是給會把作品放上網的人看的。如果你想過「這張圖流出去以後,我要怎麼證明是我的」,這頁把整套機制攤開給你看,包括它撐不住的地方。

先分清楚你要哪一種浮水印

兩件常常被混為一談的事:

這個實驗室做的是後者。因為只需要一個位元,全部的工程餘裕都可以拿去換抗破壞性:平鋪、暴力搜尋、只看整體相關性。溯源型要精準到每個位元的位置,所以對裁切、縮放特別脆弱。

要防專業的、有心的移除,兩種都不夠。開源工具防的是順手盜用。

判定要同時過兩關

驗證的輸出有兩個數字:z 是峰值強度,PSR 是峰值比旁邊突出多少。

  z PSR
正向 43.7 13.7
裁一半 19.8 12.5
裁到剩 30% 11.1 9.6
乾淨的圖 1.4(低) 10.1(高)
別人的金鑰 13.2(高) 4.4(低)

看最後兩列。兩種假陽性的破口剛好相反,只看其中一個都會被另一種騙過去。乾淨圖的分數分布很窄,所以 PSR 會衝高;別人的金鑰則相反,z 會跟著嵌入強度一起放大(你蓋得越用力,別人隨便一把金鑰撞出來的最高分也越高),可是峰值並不突出。

所以判定寫成 z > 6 而且 PSR > 6.5。實測真陽性 40/40、假陽性 0/80,最接近門檻的距離是 3.5。

第一次講錯:「那邊只看一個數字就夠了」

這個實驗室是從 LINE 對話製造機 拆出來的。那個工具會在匯出的圖上烙隱形標記,任何人都能驗出「這是合成的」,用的是固定的公開函式,判定只看 z、門檻 7。

有人問我,為什麼那邊只看一個數字就夠。我當時的回答是:製造機沒有金鑰,所以不存在「別人的金鑰來撞」這種攻擊。

我去量了。用這個 repo 的隨機金鑰蓋出來的圖,60 張裡有 16 到 25 張被製造機判成「它做的」,z 最高衝到 9.4,門檻只有 7。

原因不難懂:兩張隨機 ±1 的圖樣,總會有一部分格子剛好同號,位移搜尋又專挑最巧的那個對齊方式。

PSR 分得開,製造機自己蓋的最低 7.1,這個 repo 蓋的最高 4.4。所以正確的說法是「沒有人去撞它」,不是「不可能誤判」。這兩句話的差別,在有人真的想撞的那天會很有感。

第二次講錯:「自然圖片是低頻的,所以 logo 不能當金鑰」

第一版真的是讓人上傳 logo 當金鑰。後來放棄,我當時寫了兩個理由:

  1. 公開的東西不能當金鑰。logo 通常是公開的,任何人拿得到就能蓋出你的浮水印。
  2. 自然圖片是低頻的,logo 縮成指紋以後相鄰格子高度相關,偵測端第一步的高通會把它殺掉一截。

第二條沒有我寫的那麼致命。白化補得回大半。同一張圖上重量一次:白化前 21.8、白化後 25.8、金鑰字串 26.4,裁到剩 75% 三種都還活著。

更難看的是舊版那組數字。我原本寫「logo 版正向只有一半(25.0 對 43.7)、裁一半就驗不出來」,那是拿兩張不同的圖比出來的,根本不成立,已經更正。

所以放棄 logo 靠的是第一條理由。第二條是我自己加戲。

logo 現在留在鑰圖上當標籤,因為一資料夾長得一樣的 QR 你分不出哪張是哪張。它對演算法沒有任何貢獻,這件事我在頁面上寫清楚了。

第三次講錯:「要蒐集夠多張才可能反推」

原本的文件寫,攻擊者要蒐集夠多張蓋過的圖、取平均,才可能反推出場紋。

一張就夠。

攻擊者知道演算法,就能做跟偵測器一樣的事:高通、按 16 為週期折疊累加、取正負號,估中大約 70% 的節點。拿這個估值去驗另一張同金鑰的圖會檢出,z 落在 30 到 58,等於他手上有一把能用的金鑰。

好消息是拿去抹除只減掉一半,z 從 44 掉到 25,仍然檢出。這是平鋪式浮水印的結構性弱點,而平鋪正是它抗裁切的原因。同一個設計決定,好處跟壞處是綁在一起的。

使用者問出來的那個安全漏洞

有人問:「金鑰只有 20 個字元,指紋有 16×16 格,會不會很容易重複?」

光看大小不會。20 個字元的 Crockford base32 是 100 bit,指紋是 256 格 ±1,指紋空間大得多。

但中間那個亂數產生器把它掐掉了。舊寫法是先算出一個 32 bit 的雜湊,再從它連續吐四個數字去啟動亂數產生器。看起來有 128 bit,其實全部由那 32 bit 決定。所以指紋最多只有 2³² 種。

實測 30 萬把不同的金鑰,撞出 9 到 12 組完全一樣的指紋,2³² 的理論期望值是 10.5 組,對得上。改成跑四次獨立的雜湊、每次都吃過完整的金鑰之後,同樣 30 萬把,碰撞 0 組。

新產生的金鑰開頭是 IWL2-。舊的 IWL1- 走原本的路徑,以前蓋過的圖照樣驗得出來。修一個安全問題卻讓別人的舊作品驗不出來,那是另一種做壞。

順帶修掉的第四件事

「能裁掉多少」不能用一個百分比講,它是尺寸的函數。

存活矩陣上那個斷點(剩 75% 到 60%)是在 900×620 量出來的。同一種內容放大到 3600×2480,裁到剩 10% 還驗得出來;縮到 600×414 就幾乎裁不得。真正決定的是「剩下的格子數乘上每格的訊噪比」。

這條連帶解掉另一個原本講不清楚的建議:高細節的圖要夠大才用得了。

金鑰存成一張圖

金鑰是資料,可是存成一張圖:一串字放在記事本裡會不見,一張圖在相簿裡活得久。

secret 在鑰圖裡存三份,任何一份活著就救得回來。PNG 的 iTXt 中繼資料是主要路徑,重新上傳原檔直接讀;中繼資料被平台洗掉、或只剩一張截圖的時候,用手機掃 QR;QR 也掃不到就用眼睛抄明文。

代價是「長得像照片的鑰匙會被當照片對待」,所以那句警告是烙在像素裡的,不是寫在頁面上。說明沒人看,但圖會跟著圖跑。

分工線

要做哪一種浮水印、判定要不要收兩個數字、講錯的地方要不要留在頁面上,是我定的。演算法實作、那幾支測試(test/keyspace.mjstest/estimate-attack.mjstest/crop-vs-size.mjstest/cross-lcm.mjs)、還有整頁的視覺化,是 AI 寫的。

上面每一句「我原本寫的是錯的」,都是先有一支跑得出數字的測試,才敢那樣講。這頁最花時間的部分是把浮水印做壞給自己看,做出來反而快。

自己玩:https://yazelin.github.io/invisible-watermark-lab/,原始碼與全部測試在 https://github.com/yazelin/invisible-watermark-lab。它拆的是這個工具的另一半:LINE 對話製造機,另一半的產線寫在 一張 LINE 對話截圖,背後有五個 AI 在接力