结论
这个项目实现的是一种基于 DWT–DCT–SVD 的盲水印,更准确地说,是利用奇异值余数进行量化编码。它不是加密系统,也不是不可伪造的版权证明。
存在明显的设计层面弱点:
想让原水印无法再被正确提取,相对容易;
想覆盖、伪造另一个水印,也比较容易;
默认或弱密码可以直接猜测;
强随机密码下,从单张图片完整恢复原水印和密钥不一定能“一键破解”,但密钥设计仍达不到现代密码学安全标准;
对旋转、裁剪、平移等攻击的所谓鲁棒性,很多情况下依赖先恢复原有几何位置,并非直接从攻击后的图片中提取。
因此,它适合普通防转载标记、实验和非对抗场景,不适合单独承担司法取证、版权归属或高价值内容保护。
一、它是怎么加水印的
- 图像变换
源码首先把图像转换成 YUV,然后对三个通道分别做 Haar 小波变换 DWT。它只处理低频近似分量 CA,再将该分量切成多个 4×4 小块。
选择低频的原因是:低频信息在 JPEG 压缩、轻微噪声、亮度变化中通常比较稳定,但修改过强也更容易产生肉眼可见的失真。
- 每个小块做 DCT 和 SVD
每个 4×4 小块执行:
-
DCT;
-
使用 password_img 生成的排列打乱 16 个 DCT 系数;
-
对打乱后的矩阵做 SVD;
-
修改前两个奇异值;
-
逆 SVD、逆排列、逆 DCT。
password_img 实际只是 NumPy 随机数生成器的 seed,用它为每个小块生成一个系数排列。
- 水印位写入奇异值余数
项目默认量化步长是:
第一奇异值:d1 = 36
第二奇异值:d2 = 20
对于水印位 ,核心操作等价于:
s'=\left(\left\lfloor\frac{s}{d}\right\rfloor+\frac14+\frac12w\right)d
因此:
bit 0 被放到一个量化区间约 1/4 的位置;
bit 1 被放到约 3/4 的位置。
提取时判断 s % d 是否超过 d/2。第二奇异值也做相同处理,最后按约 3:1 的权重合并。
这实际上是一种类似 **QIM(量化索引调制)**的编码方式。
- 同一个水印被反复写入
水印位不是只写一次。代码将水印循环嵌入所有可用小块,并在三个 YUV 通道中都写入:
wm_1 = self.wm_bit[i % self.wm_size]
提取时,把三个通道及所有重复位置的结果平均,以抵抗局部遮挡、噪声和部分裁剪。
水印越短,重复次数越多,通常越鲁棒;水印越长,冗余越低。
- 两个所谓“密码”
password_wm:只负责打乱水印 bit 的次序;
password_img:控制每个 DCT 小块内的系数排列。
水印加密本身只是调用 RandomState(password_wm).shuffle(...),不是真正的加密算法。
提取还必须知道水印长度或二维形状 wm_shape,所以这里的“盲”仅表示不需要原始图像,不表示什么参数都不需要。
二、主要安全问题
- 密码不是密码学密钥
项目直接把整数传给 np.random.RandomState。NumPy 官方将 RandomState 定义为旧版、固定行为的 MT19937 伪随机生成器;整数 seed 的范围最多为 0 到 2^32-1。
问题包括:
没有 Argon2、scrypt、PBKDF2 等密钥派生;
没有盐;
没有每张图片独立的随机 nonce;
同一密码会重复生成相同的排列策略;
最大整数 seed 只有 32 位;
默认值以及官方示例都是 1。
所以:
使用 1、1234、时间戳、用户 ID 等密码,基本等于没有密码;
即使使用完整随机 32 位整数,也不属于合理的现代密码学安全级别;
两个 seed 虽然可以不同,但相关攻击通常可以分阶段验证,不能简单理解为完整的 64 位安全。
- 覆盖攻击可以破坏原水印
攻击者不一定要恢复原密钥。只要再次运行类似的嵌入过程,就会重新量化同一批频域特征,从而破坏原来的奇异值余数关系。
项目已有 issue 报告:在已经加水印的图片上使用不同密码再次加水印,可以提取新水印,同时旧水印可能无法恢复。
讨论中也指出,二次写入本身相当于对原频域特征实施攻击。
这意味着从攻击者角度,让原水印失效通常比破解并读出水印更容易。
- 几何同步非常脆弱
它依赖固定的小波网格和固定 4×4 分块。一旦发生:
平移若干像素;
裁剪;
旋转;
透视变换;
非整数比例缩放;
截图时增加边框;
提取器看到的小块边界便可能和嵌入时完全错位。这类攻击称为同步攻击。
项目示例中的“抗旋转、抗裁剪”实际上常常是:
先知道旋转角度,再反向旋转;
先恢复到原始尺寸;
知道裁剪坐标,或利用原水印图片估计裁剪位置;
将裁剪区域填回原图位置后再提取。
所以 README 中的“抗旋转、裁剪”不能理解为攻击后的图片可以直接解码。
2025 年的一个独立测试 issue 也报告,直接进行 45°、180°旋转及裁剪后,提取结果出现明显错误。
- 转码和平台处理可以导致提取失败
实际社交平台通常会组合执行:
JPEG 重压缩;
缩放;
锐化或降噪;
色彩空间转换;
元数据和透明通道处理;
局部裁剪。
仓库 issue 中有用户报告:微信非原图传输、小红书发布以及截图后,水印无法正常解析;只有全程按原图传输才能恢复。
这说明其鲁棒性对参数、图片内容、输出格式和平台处理链高度敏感。
- 没有真正的“水印存在性检测”
字符串和 bit 模式提取时,代码会:
-
对所有位置计算一个数值;
-
用一维 K-means 强行分成两类;
-
输出 0/1 bit。
它没有:
统计显著性阈值;
相关系数门槛;
置信度;
校验和;
MAC;
数字签名;
错误检测码。
因此,给一张根本没有水印的图片,提取程序仍然会产生一串 bit。它无法严格回答:
“这张图是否确实包含由某人嵌入的水印?”
它只能回答:
“按照给定 seed 和长度,把图像特征解释成 bit 后得到什么?”
这会产生误判和选择性尝试参数的问题。
- 水印内容可以伪造,不能证明归属
任何人都可以把“张三版权所有”写进任意图片。算法没有私钥签名,也没有可信时间戳,更没有将水印与原始文件哈希绑定。
所以即使成功提取出某个名字,也不能单独证明:
谁先拥有图片;
谁首先嵌入;
图片是否被二次覆盖;
水印是否由权利人写入;
提取结果是否经过参数筛选。
这不是实现 bug,而是整个认证协议的缺失。
- 多副本合谋攻击
假如攻击者获得同一张原图的多个水印版本,例如分发给多个用户的不同编号版本,可以对这些图片逐像素或在频域中做:
平均;
中值;
差分分析;
异常值剔除。
由于该项目的嵌入是确定性的、没有每图随机化,且水印扰动集中在固定变换结构中,多副本会帮助估计无水印载体并削弱水印。
这是根据源码结构可以推导出的攻击面,不是仓库中已经证明的完整密钥恢复漏洞。
三、“破解”难度要按目标区分
攻击目标 评估
让合法提取者提取失败 较容易,二次嵌入、几何错位、转码和滤波都可能实现
覆盖成攻击者自己的水印 容易到中等
使用默认密码提取 非常容易
猜测常见整数密码 容易
从单张图片恢复强随机密码 不一定立即成功,但只有 32 位 seed,仍不符合密码学安全要求
不知道长度时恢复完整文字 难度提高,但长度不是安全密钥,可通过候选长度和文本结构测试
伪造“版权所有者”声明 容易,因为没有签名或可信登记
证明图片确实含有某个合法水印 现有接口无法提供严格统计证明
最现实的风险排序通常是:
-
破坏水印;
-
覆盖或伪造;
-
利用默认/弱 seed 解码;
-
才是完整恢复高熵 seed。
四、用于真实版权场景时怎么改
至少需要增加以下机制:
-
水印内容不直接写姓名
写入随机资产 ID、版本号、时间戳、图片哈希摘要,以及这些字段的数字签名。
-
使用真正的密钥体系
采用至少 128 位随机密钥;口令需经过 Argon2id 或 scrypt;通过 HKDF 派生每张图片的独立密钥。
-
加入每图 nonce
避免不同图片重复使用完全相同的排列和载体位置。
-
加入纠错编码
例如 BCH、LDPC 或 Reed–Solomon,加交织以抵御局部损坏。当前实现主要依靠简单重复平均。
-
加入同步模板或特征点配准
在提取前使用尺度、旋转和平移不变的同步结构,而不是假设像素网格仍然对齐。
-
加入认证和检测分数
输出相关性、置信度和假阳性概率;对消息验证 MAC 或数字签名,不能只是输出一串 bit。
-
保存外部证据链
保存原始文件哈希、签名时间、登记记录、版本历史和分发日志。盲水印只作为辅助证据。
总体判断是:这个项目的信号处理思路合理,作为实验性鲁棒水印库有价值;但从安全工程角度,它可被移除、覆盖和伪造,密码部分也不能视为真正的加密。
结论
这个项目实现的是一种基于 DWT–DCT–SVD 的盲水印,更准确地说,是利用奇异值余数进行量化编码。它不是加密系统,也不是不可伪造的版权证明。
存在明显的设计层面弱点:
想让原水印无法再被正确提取,相对容易;
想覆盖、伪造另一个水印,也比较容易;
默认或弱密码可以直接猜测;
强随机密码下,从单张图片完整恢复原水印和密钥不一定能“一键破解”,但密钥设计仍达不到现代密码学安全标准;
对旋转、裁剪、平移等攻击的所谓鲁棒性,很多情况下依赖先恢复原有几何位置,并非直接从攻击后的图片中提取。
因此,它适合普通防转载标记、实验和非对抗场景,不适合单独承担司法取证、版权归属或高价值内容保护。
一、它是怎么加水印的
源码首先把图像转换成 YUV,然后对三个通道分别做 Haar 小波变换 DWT。它只处理低频近似分量 CA,再将该分量切成多个 4×4 小块。
选择低频的原因是:低频信息在 JPEG 压缩、轻微噪声、亮度变化中通常比较稳定,但修改过强也更容易产生肉眼可见的失真。
每个 4×4 小块执行:
DCT;
使用 password_img 生成的排列打乱 16 个 DCT 系数;
对打乱后的矩阵做 SVD;
修改前两个奇异值;
逆 SVD、逆排列、逆 DCT。
password_img 实际只是 NumPy 随机数生成器的 seed,用它为每个小块生成一个系数排列。
项目默认量化步长是:
第一奇异值:d1 = 36
第二奇异值:d2 = 20
对于水印位 ,核心操作等价于:
s'=\left(\left\lfloor\frac{s}{d}\right\rfloor+\frac14+\frac12w\right)d
因此:
bit 0 被放到一个量化区间约 1/4 的位置;
bit 1 被放到约 3/4 的位置。
提取时判断 s % d 是否超过 d/2。第二奇异值也做相同处理,最后按约 3:1 的权重合并。
这实际上是一种类似 **QIM(量化索引调制)**的编码方式。
水印位不是只写一次。代码将水印循环嵌入所有可用小块,并在三个 YUV 通道中都写入:
wm_1 = self.wm_bit[i % self.wm_size]
提取时,把三个通道及所有重复位置的结果平均,以抵抗局部遮挡、噪声和部分裁剪。
水印越短,重复次数越多,通常越鲁棒;水印越长,冗余越低。
password_wm:只负责打乱水印 bit 的次序;
password_img:控制每个 DCT 小块内的系数排列。
水印加密本身只是调用 RandomState(password_wm).shuffle(...),不是真正的加密算法。
提取还必须知道水印长度或二维形状 wm_shape,所以这里的“盲”仅表示不需要原始图像,不表示什么参数都不需要。
二、主要安全问题
项目直接把整数传给 np.random.RandomState。NumPy 官方将 RandomState 定义为旧版、固定行为的 MT19937 伪随机生成器;整数 seed 的范围最多为 0 到 2^32-1。
问题包括:
没有 Argon2、scrypt、PBKDF2 等密钥派生;
没有盐;
没有每张图片独立的随机 nonce;
同一密码会重复生成相同的排列策略;
最大整数 seed 只有 32 位;
默认值以及官方示例都是 1。
所以:
使用 1、1234、时间戳、用户 ID 等密码,基本等于没有密码;
即使使用完整随机 32 位整数,也不属于合理的现代密码学安全级别;
两个 seed 虽然可以不同,但相关攻击通常可以分阶段验证,不能简单理解为完整的 64 位安全。
攻击者不一定要恢复原密钥。只要再次运行类似的嵌入过程,就会重新量化同一批频域特征,从而破坏原来的奇异值余数关系。
项目已有 issue 报告:在已经加水印的图片上使用不同密码再次加水印,可以提取新水印,同时旧水印可能无法恢复。
讨论中也指出,二次写入本身相当于对原频域特征实施攻击。
这意味着从攻击者角度,让原水印失效通常比破解并读出水印更容易。
它依赖固定的小波网格和固定 4×4 分块。一旦发生:
平移若干像素;
裁剪;
旋转;
透视变换;
非整数比例缩放;
截图时增加边框;
提取器看到的小块边界便可能和嵌入时完全错位。这类攻击称为同步攻击。
项目示例中的“抗旋转、抗裁剪”实际上常常是:
先知道旋转角度,再反向旋转;
先恢复到原始尺寸;
知道裁剪坐标,或利用原水印图片估计裁剪位置;
将裁剪区域填回原图位置后再提取。
所以 README 中的“抗旋转、裁剪”不能理解为攻击后的图片可以直接解码。
2025 年的一个独立测试 issue 也报告,直接进行 45°、180°旋转及裁剪后,提取结果出现明显错误。
实际社交平台通常会组合执行:
JPEG 重压缩;
缩放;
锐化或降噪;
色彩空间转换;
元数据和透明通道处理;
局部裁剪。
仓库 issue 中有用户报告:微信非原图传输、小红书发布以及截图后,水印无法正常解析;只有全程按原图传输才能恢复。
这说明其鲁棒性对参数、图片内容、输出格式和平台处理链高度敏感。
字符串和 bit 模式提取时,代码会:
对所有位置计算一个数值;
用一维 K-means 强行分成两类;
输出 0/1 bit。
它没有:
统计显著性阈值;
相关系数门槛;
置信度;
校验和;
MAC;
数字签名;
错误检测码。
因此,给一张根本没有水印的图片,提取程序仍然会产生一串 bit。它无法严格回答:
它只能回答:
这会产生误判和选择性尝试参数的问题。
任何人都可以把“张三版权所有”写进任意图片。算法没有私钥签名,也没有可信时间戳,更没有将水印与原始文件哈希绑定。
所以即使成功提取出某个名字,也不能单独证明:
谁先拥有图片;
谁首先嵌入;
图片是否被二次覆盖;
水印是否由权利人写入;
提取结果是否经过参数筛选。
这不是实现 bug,而是整个认证协议的缺失。
假如攻击者获得同一张原图的多个水印版本,例如分发给多个用户的不同编号版本,可以对这些图片逐像素或在频域中做:
平均;
中值;
差分分析;
异常值剔除。
由于该项目的嵌入是确定性的、没有每图随机化,且水印扰动集中在固定变换结构中,多副本会帮助估计无水印载体并削弱水印。
这是根据源码结构可以推导出的攻击面,不是仓库中已经证明的完整密钥恢复漏洞。
三、“破解”难度要按目标区分
攻击目标 评估
让合法提取者提取失败 较容易,二次嵌入、几何错位、转码和滤波都可能实现
覆盖成攻击者自己的水印 容易到中等
使用默认密码提取 非常容易
猜测常见整数密码 容易
从单张图片恢复强随机密码 不一定立即成功,但只有 32 位 seed,仍不符合密码学安全要求
不知道长度时恢复完整文字 难度提高,但长度不是安全密钥,可通过候选长度和文本结构测试
伪造“版权所有者”声明 容易,因为没有签名或可信登记
证明图片确实含有某个合法水印 现有接口无法提供严格统计证明
最现实的风险排序通常是:
破坏水印;
覆盖或伪造;
利用默认/弱 seed 解码;
才是完整恢复高熵 seed。
四、用于真实版权场景时怎么改
至少需要增加以下机制:
水印内容不直接写姓名
写入随机资产 ID、版本号、时间戳、图片哈希摘要,以及这些字段的数字签名。
使用真正的密钥体系
采用至少 128 位随机密钥;口令需经过 Argon2id 或 scrypt;通过 HKDF 派生每张图片的独立密钥。
加入每图 nonce
避免不同图片重复使用完全相同的排列和载体位置。
加入纠错编码
例如 BCH、LDPC 或 Reed–Solomon,加交织以抵御局部损坏。当前实现主要依靠简单重复平均。
加入同步模板或特征点配准
在提取前使用尺度、旋转和平移不变的同步结构,而不是假设像素网格仍然对齐。
加入认证和检测分数
输出相关性、置信度和假阳性概率;对消息验证 MAC 或数字签名,不能只是输出一串 bit。
保存外部证据链
保存原始文件哈希、签名时间、登记记录、版本历史和分发日志。盲水印只作为辅助证据。
总体判断是:这个项目的信号处理思路合理,作为实验性鲁棒水印库有价值;但从安全工程角度,它可被移除、覆盖和伪造,密码部分也不能视为真正的加密。