64位决定你能上传什么到互联网:Chat Control背后的算法
2021年8月,苹果宣布将在每张照片上传到iCloud之前扫描你的iPhone照片。同年9月,在两周的争议后撤回了提案。2022年12月,官方宣告计划终止。与此同时,2022年,欧盟委员会提出了一项规定,要求WhatsApp、Signal和Telegram执行与苹果撤销的提案完全相同的操作。这项法规,即Chat Control,目前仍在欧洲理事会中推进讨论。 这两项尝试都依赖同一个算法:一个实现于40行Python代码的64位感知哈希算法。这不是夸张,甚至不是比喻。确实只有40行,在阅读完这篇文章后,您可以在自己的机器上运行它的实现版本。 大量的公众讨论——无论是在西班牙还是其他地方——都发生在参与者对该机制的工作原理不了解的情况下。法规的讨论充满了政治色彩(“隐私与儿童安全”对立),而技术核心却如同黑箱。本篇文章将扮演该黑箱的解药。如果读完后您想要发表观点,您可以基于技术事实。如果不想,那至少您已经了解他们让您接受的是什么。 问题:SHA-256无法识别两张图片是同一张 任何开发者第一个冲动的解决方案可能是用SHA-256。如果两张图片生成了相同的哈希值,那么它们就是同一张图片。简单、快速、加密安全。 但这不起作用。 import hashlib def sha256(path): with open(path, "rb") as f: return hashlib.sha256(f.read()).hexdigest() # 同一张图片,不同压缩 sha256("foto.jpg") # "a7f3..." sha256("foto_reencoded.jpg") # "9c2e..." (完全不同) 只改变一个比特——例如JPEG压缩质量从95降低到90,或者从WebP格式转换为JPEG,甚至亮度的微小调整——SHA-256生成的哈希值都会完全不同。数据清晰地证明:加密相似性设计为完全不容忍差异,这是一种特性,而非漏洞。 为了检测“相同的图片即使压缩不同”,需要一个容忍度更高的算法。这意味着需要一个感知哈希:一个能在图片内容不变而细微改变的情况下仍然保持一致的64位签名。 aHash:亮度平均值 最早有用的方案被称为平均哈希(Average Hash)或aHash。其理念非常简单: import numpy as np from PIL import Image def average_hash(path, size=8): img = Image.open(path).convert("L").resize((size, size)) arr = np.asarray(img, dtype=np.float32) mean = arr.mean() bits = (arr.flatten() > mean).astype(int) return "".join(str(b) for b in bits) def hamming(a, b): return sum(x != y for x, y in zip(a, b)) 图像被缩小为8×8像素的灰度图,计算亮度平均值,每个像素亮度大于平均值时为1,否则为0。结果是64位,代表全局亮度分布。 两张“相同”的图片会生成汉明距离很小的哈希值(≤ 5 位不同)。两张不同的图片则会有大的汉明距离(40+ 位)。这种方法能在图片被缩放、稍微裁剪或者重新编码时起作用。但如果有人调整了对比度或全局亮度,aHash就会失效。这是第一步。 dHash:相对梯度 下一步是2011年由Neal Krawetz提出的差异哈希(Difference Hash),简称dHash: ...